8000 Tags · d0minikk/sure · GitHub
[go: up one dir, main page]

Skip to content

Tags

Tags

v0.7.5-alpha.7

Toggle v0.7.5-alpha.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.7.5-alpha.6

Toggle v0.7.5-alpha.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.7.4

Toggle v0.7.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Final `v0.7.4` version

v0.7.3

Toggle v0.7.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Stable release version 0.7.3

v0.7.2

Toggle v0.7.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Helm CHANGELOG

v0.7.1-hotfix.1

Toggle v0.7.1-hotfix.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Bump hotfix version

v0.7.1

Toggle v0.7.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Bad luck to prepare for `v0.7.1`?

v0.7.0-hotfix.3

Toggle v0.7.0-hotfix.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
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>

v0.7.0

Toggle v0.7.0's commit message
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>

v0.6.9

Toggle v0.6.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Final v0.6.9 release versioning

0