Repository navigation
Tags
Tags
feat(monobank): add monobank (UA) bank sync (we-promise#3386) * feat(monobank): add monobank (UA) provider integration Adds a per-family Monobank connection built on monobank's personal API (https://api.monobank.ua/docs), modelled on the Up Bank integration. Users bring their own token from https://api.monobank.ua/; cards and jars are imported as cash accounts and their statements as transactions. monobank's rate limit shapes the sync design. Each personal endpoint accepts one request per minute and a statement request covers at most 31 days, so: * client-info is fetched once per sync and reused for every account snapshot; * each linked account gets a forward window (new activity, always re-covering the last few days so unsettled holds stay visible) plus, when budget allows, one backward window that walks older history 31 days at a time; * statement requests are capped per sync (4 by default), least-recently-covered accounts first, so a family with many accounts cannot turn one sync into an hour of throttled sleeping. Deferred accounts and throttling are skips, not failures. Provider specifics: * Balances subtract the credit limit monobank reports inside `balance`, leaving own funds. Jars map to savings, cards to checking. * `hold: true` becomes a pending transaction. monobank may settle a hold under a different id, so pending entries are pruned when the hold drops out of the latest statement window rather than matched up by id. * Categories are derived from the merchant's MCC. The matcher resolves Sure's default category name in the family's own locale before falling back to English aliases, since a monobank user's categories are unlikely to be named in English. MCCs without an honest equivalent (cash withdrawals, transfers, gambling) stay uncategorised. * No FX metadata: the statement reports `operationAmount` but never the operation currency, so the raw amount is recorded and fx_from/fx_amount are left unset instead of guessing. * The client's personal name is dropped before the client-info payload is stored. Also adds IsoNumericCurrency, a shared concern that maps ISO 4217 numeric currency codes (980) onto the alphabetic codes Sure stores, since CurrencyNormalizable only accepts the latter. Unlike the Up controller this one has no index/show/new/edit actions: nothing links to them and they have no templates. Connections are managed from the providers settings panel. Configuration lives in config/initializers/monobank.rb (MONOBANK_INCLUDE_PENDING, MONOBANK_MAX_STATEMENT_REQUESTS_PER_SYNC, MONOBANK_PENDING_LOOKBACK_DAYS, MONOBANK_INITIAL_HISTORY_DAYS, MONOBANK_MIN_REQUEST_INTERVAL, MONOBANK_DEBUG_RAW) and is documented in docs/hosting/monobank.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(monobank): close sync gaps and stop losing pending transactions Statement windows and pending handling had several ways to silently lose data. Cursors and windows: * history_synced_from becomes a datetime. monobank's window is 31 days *plus one hour*, so a backward step landed at 23:00 on the day before its intended boundary while a day-granular cursor stored that whole day as covered. Every step left a 23-hour hole, and a year of backfill is about 12 steps. Consecutive windows are now contiguous to the second. The migration is edited in place rather than followed by a second one, since it has not shipped yet. * A forward window wider than the 31-day cap was clamped up, but the non-truncated branch then set statement_synced_through to now and left history_synced_from alone, so both cursors claimed the skipped interval as covered while the backward walk only ever went below it. Reachable through paused jobs, a hidden account, or a connection left in requires_update for a month. The clamped start is now handed to the backward walk; store_transactions dedupes by canonical id, so re-fetching is harmless. * A response at the ~500-item cap only accounts for its newest part, but both truncated branches stored the *date* of the oldest record received and so claimed the unfetched remainder of that day. They store its timestamp now. * Date-to-time conversions use in_time_zone rather than to_time, which read the date in the server process's own zone and moved the window whenever TZ differed between web and worker. Pending transactions: * A held transaction was discarded whenever it stopped appearing in the payload, even when the window just fetched never reached its date. A hotel, car hire or fuel pre-authorization outlives the three-day lookback easily, so the hold left the payload on day four and its entry was pruned while the funds were still blocked upstream. The forward window now reaches back to the oldest hold on file, and a hold below what the response actually covered is kept rather than assumed settled (the pairing RedbarkItem::Importer uses). A hold older than one statement window is still retired, since monobank can no longer be asked about it and the entry would otherwise never clear. * monobank was added to Transaction::PENDING_PROVIDERS but not to the three hard-coded provider lists inside Account::ProviderImportAdapter's pending lookups, leaving it the only pending-capable provider with no pending→posted reconciliation and no duplicate suggestion. Since monobank may settle a hold under a different id, the settled record created a brand-new entry, losing the user's category and notes, and the prune then deleted the pending one. The fragment is derived from PENDING_PROVIDERS so the next provider cannot drift out of sync the same way; it stays a frozen constant built from a frozen list, so no user input reaches the SQL. * prune_stale_pending_entries destroyed every stale pending entry with no protection check, while the import adapter refuses to overwrite an entry that is excluded, user-modified or import-locked. A split pending hold (splitting sets both flags), a user-edited entry or a transfer leg was hard-deleted, taking its child entries (dependent: :destroy) and any transfer (on_delete: :cascade) with it, silently. Those entries are now kept and only lose their pending flag, which is all the hold's disappearance tells us; the count is reported in the processor's stats and captured as a DebugLogEntry. UpAccount and AkahuAccount share this flaw — this code was modelled on Up — but fixing them belongs in its own change. Throttling and malformed responses: * A throttled client-info failed the whole sync. monobank allows one request per minute per endpoint and the timer lives on the client instance, so a manual sync landing in the same minute as a scheduled one collides legitimately. Statement throttling was already a skip; client-info is one too, leaving the connection healthy for the next sync. * The setup and account-picker screens failed closed: on any API error they hid the account list already in the database and disabled the submit button. Since the very first run reliably collides with its own rate limit — create schedules a sync that spends the client-info request, then the setup screen asks for it again inside the same minute — this blocked the default path through setup. They now render the stored accounts and show the failure as a notice. * get_statement never checked the type of the parsed body, so a 204, an empty 200 or a JSON error object became [] — indistinguishable from an account with no activity — and the importer then recorded the window as permanently covered. It mirrors the guard get_client_info already had: :parse_error is a hard failure, which leaves the cursors untouched so the window is walked again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHYHrMigS91VZYrqoTA7B3 * chore(monobank): add breadcrumb label, fixtures and encryption coverage Brings the Monobank integration in line with the conventions the other provider integrations follow: - breadcrumbs.monobank_items so the settings crumb reads "monobank" instead of the titleized controller name - monobank_items/monobank_accounts fixtures, matching the shape used by the other provider fixtures - encryption_verification_test coverage for the item credentials and the account payloads, masked PAN and IBAN * fix(monobank): address review feedback Correctness: - A backward history window that hard-fails (auth, parse, provider error) no longer collapses into an empty success. The forward window is still persisted first — that activity is already in hand — and the failure is then propagated so the sync counts it instead of reporting healthy while silently never fetching the configured history. A throttled backward walk stays a soft skip, matching budget exhaustion. - The three AccountProvider.create! paths handle RecordNotUnique. The presence checks guarding them are reads, so a double submit gets both requests past them and the unique indexes settle it; the request that loses now gets the "already linked" flash instead of a 500. Conventions: - Sync progress text goes through I18n (en + uk) rather than hard-coded English, matching the Redbark/Brex/Binance syncers. - Per-account import failures reach DebugLogEntry.capture, so support can see which account failed and why in /settings/debug. - The provider badge uses bg-primary rather than bg-black: the badge text is text-inverse, which flips with the theme, so a hardcoded black background rendered dark-on-dark. Matches trade_republic. Tests: - Cover the three backward-window outcomes (hard failure reported, forward window still persisted, rate limit stays a success). - Assert SyncJob/DestroyJob are actually enqueued instead of stubbing them, which allowed zero calls. - Encryption tests read the columns through the connection and assert the stored bytes are not the plaintext value; a round-trip through the accessor passes just as happily against an unencrypted column. - Bind the timestamp once in the truncated-window test: evaluating 10.days.ago separately for fixture and assertion raced the wall clock. Docs: the personal token can also register a statement webhook, so "read-only, nothing else" was wrong. * test(monobank): replace IBAN-shaped test values with placeholders Pipelock's DLP scan flags the IBAN pattern in the PR diff. The values were synthetic, but the shape is what the scanner matches, and a test asserting a column is encrypted has no need for a realistic IBAN — nothing in the code parses or validates one. Swapped every IBAN-shaped literal on the branch for an obviously-fake placeholder, so the scan is deterministic and no reader has to work out whether a real Ukrainian IBAN reached the repository. * fix(monobank): route provider client diagnostics to the debug log The four Rails.logger calls in Provider::Monobank were the last raw-logger spots in the file. Per the project's provider-diagnostics convention they now go through DebugLogEntry.capture, so a failed request is inspectable from /settings/debug rather than only from the application log. The client has no family or account_provider of its own — MonobankItem::Importer already records the same failure against the connection when it catches the typed error. What is captured here is the transport detail that never reaches that layer: the HTTP status, the retry attempt, and the size of a body that would not parse. The body itself is never recorded; it carries statement PII. handle_response and parse_response_body now take the operation name from #get so an entry says which endpoint failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmUhCuVYR9dmhrrCGFDrW * fix(monobank): report monobank sync health on the providers page "monobank" was in PANEL_SYNCABLE_TYPES but missing from family_panel_items, so compute_provider_sync_health skipped it. A monobank connection whose last sync failed or went stale still rendered as :ok on /settings/providers, with no error badge and no "needs attention" grouping. Added a test asserting every syncable family panel supplies its items, so the next provider cannot land with the same half-wiring. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmUhCuVYR9dmhrrCGFDrW * refactor(monobank): collapse duplicate account-fetch rescue branches The Provider::Monobank::Error and StandardError branches wrote the same DebugLogEntry and returned the same message, differing only in wording that error_class already carries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmUhCuVYR9dmhrrCGFDrW * fix(monobank): close review gaps in the debug-logging sweep Three findings from an independent review of the previous commits: - MonobankItem::Importer#import_transactions had a rescue that only wrote to Rails.logger. fetch_window captures provider errors itself, so what reaches this branch is a persistence failure — an invalid snapshot, a cursor that will not save — which showed up as an incremented failure count and nothing in /settings/debug. It now calls capture_sync_error like every other rescue in the file. - The providers-page regression test compared two hash literals, so it stayed green for "monobank" => nil or a controller that stopped assigning @monobank_items. Replaced with a behavioural test: a failed sync must reach provider_sync_health and put the panel in needs_attention. Verified it fails both ways. - The retry-attempt entry is a transient note, not a failure, so it files under "provider_sync" like the importer's other non-error notes; the jittered delay is rounded; and the Rails.logger.warn line is back alongside it, which is what Provider::Lunchflow does and what makes a retry visible in bin/dev. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XvmUhCuVYR9dmhrrCGFDrW * fix(monobank): capture failed requires_update flips in the debug log MonobankItem::Importer#mark_requires_update! swallowed a failed status update with only a Rails.logger line. That leaves a connection with a rejected token rendering as healthy and nothing for support to find in /settings/debug. It now goes through capture_sync_error like every other rescue in the importer, with a test that fails against the old code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q1FCXxdkbyPUJqgfroH1D * fix(monobank): attach account_provider when a requires_update flip fails mark_requires_update! now takes an optional monobank_account and passes it through to capture_sync_error, so a failed status flip triggered from the statement path records account_provider in the debug log. The client-info path is unchanged and keeps the nil default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Q1FCXxdkbyPUJqgfroH1D --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(rules): keep dynamically added nested-attribute keys numeric (we-… …promise#3444) * fix(rules): keep dynamically added nested-attribute keys numeric Rails strong params only treat integer-keyed hashes as nested attributes (ActionController::Parameters.nested_attribute? matches /\A-?\d+\z/), so rules_controller.js keys of the form "new_1"/"new_2" (introduced in 457698f, PR we-promise#3397) were silently dropped on submit: creating a rule from the modal failed with "must have at least one action" even though an action was present, and the re-rendered form lost the entered rows. Date.now() alone can repeat within the same millisecond and a small counter alone can collide with the numeric indexes Rails assigns to persisted rows on edit forms, so combine them (Date.now() * 1000 + seq) in both rules_controller.js and rule/conditions_controller.js. Adds a system test that creates a rule through the modal with dynamically added rows; no system test previously covered this path, which is why the regression shipped. * Fix rule creation regression --------- Co-authored-by: Instinct Agent <agent@instinct.com>
refactor(i18n): standardize product name placeholder in import mappin… …gs (we-promise#1631) (we-promise#1802) Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
fix(mobile): stop app icon source overwrite (we-promise#1594) * fix(mobile): use colored iOS app icon source * Revert "fix(mobile): use colored iOS app icon source" This reverts commit fc646f6. * fix(mobile): provide iOS dark launcher icon variant * fix(ci): stop overwriting mobile icon source for TestFlight --------- Co-authored-by: SureBot <sure-bot@we-promise.com>
PreviousNext