Skip to content

Release: develop -> main - #4450

Merged
TaprootFreak merged 4 commits into
mainfrom
develop
Jul 29, 2026
Merged

Release: develop -> main#4450
TaprootFreak merged 4 commits into
mainfrom
develop

Conversation

@github-actions

Copy link
Copy Markdown

Automatic Release PR

This PR was automatically created after changes were pushed to develop.

Commits: 1 new commit(s)

Checklist

  • Review all changes
  • Verify CI passes
  • Approve and merge when ready for production

* fix(kyc): skip the Sumsub file sync for non-Sumsub ident steps

reviewIdentSteps selects every ident step in internal review, regardless of
its type, and syncs the ident files before persisting the new status.
syncIdentFilesInternal throws for any step that is not a Sumsub one, so a
manual ident lost its manual-review transition and stayed in internal review -
where the every-minute review picked it up and failed it again, indefinitely.

Only call the sync for Sumsub steps: a manual ident uploads its document in
updateIdentManual and never has an IDENT_REPORT to sync. The ordering stays as
it is, so a failing sync still keeps a Sumsub step in internal review to be
retried.

* fix(kyc): document the intentional sync-before-save ordering, tighten the tests

- state in the comment that running the sync before the save is deliberate, so the
  guard is not mistaken for a workaround and the ordering is not "cleaned up" later
- cover that ordering: a Sumsub step whose sync fails must stay unsaved for a retry
- assert the status the repo actually receives; the entity is already mutated in
  memory before the sync, so the previous assertion held with the bug present
- use the SumSubLevelName enum instead of a level name that cannot occur
- name the spy after the method it replaces, add the dep-scope comment the other
  blocks in this file carry, and drop the unused Config bootstrap

* fix(kyc): cover both Sumsub ident types and make the retry test self-validating

- run the two Sumsub cases over SumsubAuto and SumsubVideo: a guard narrowed to
  one of them passed the previous tests, while the other type would have
  completed without any file and without an error
- assert in the retry case that the sync was reached, so its two absence
  assertions cannot hold because the iteration never got there
- type savedStatus as optional, since undefined is the "never saved" sentinel
- state in the dep-scope comment which stubs the short dep list depends on

* fix(kyc): pin the remaining legs of the ident file-sync condition

Mutation-checking the previous tests showed four weakenings of the condition
that no test noticed: replacing the type guard with !isManual, dropping the
already-has-a-report check, matching a different file subType, and dropping the
isCompleted leg. Each of them re-breaks a real path.

- run the non-Sumsub case over Manual and both legacy IdNow types
- cover a step that already carries its ident report, i.e. the normal case in
  which the webhook downloaded it before the review runs
- cover a completing step, which needs its files even more than one in manual
  review: it leaves internal review for good and is never revisited

* fix(kyc): pin the ident file-sync condition against its last three weakenings

Three more mutations of the condition passed the previous cases: collapsing the
report check to "the account has no files at all", and replacing the whole
status clause with true or with !isFailed. The first would disable the
review-time sync for nearly every account, the other two would pull documents
for an ignored ident.

- give the no-report cases an unrelated file, so an empty file list cannot stand
  in for a missing ident report
- cover the ignored outcome, the third one the review can reach
- separate the auto and video IdNow fixtures, which are told apart by companyid

* fix(kyc): use a synthetic step id and an exhaustive fixture map

- the fixture carried a real step id from the incident; fixtures stay synthetic
- map every ident type without the optional modifier, so a sixth type fails to
  compile here instead of silently reaching the review without a result
TaprootFreak and others added 3 commits July 29, 2026 15:37
* feat(custody): expose order timestamps in the account history

The order history DTO carried no timestamp at all, so a client had no way
to date an entry. Pass both the creation date and the valuta timestamp
separately rather than collapsing them: an order only gets completedAt
once it completes, and the distinction between when an order was recorded
and when it was valuated is exactly what a reader needs.

* test(custody): pin the history mapper timestamps

The valuta timestamp is only ever set once an order completes. A later
refactoring that collapses it onto the creation date would claim a valuta
that never happened, so assert the undefined case explicitly.
* fix(scrypt): back off quarantined-order venue lookups and release the never-sent order 127664

A quarantined order whose reference the venue does not know pays a full
30-day execution-report fetch on every reconciliation pass — several per
10-second tick via the pipeline loop, measured at ~300 heavy fetches per
hour against the same connection whose failure usually caused the
quarantine in the first place.

- resolveUncertainOrders: per-order cooldown, a tenth of the order's age
  clamped to [1 min, 30 min], stamped when the lookup FINISHES (success,
  inconclusive and thrown alike); a pending manual release bypasses it
- getOrderStatus: optional 'since' lower bound for the fallback fetch;
  the uncertain-order path passes order.created minus one day, never
  wider than the previous fixed 30-day window
- migration: release order 127664 — provably never sent (its stored
  failure is thrown before any bytes leave the process, the venue never
  knew the reference across 8 h of lookups, the EUR position never
  drained and was manually converted at the venue). Compare-and-set
  guards make it a no-op if the admin endpoint releases it first;
  correlationId + created window pin the exact production row.

* fix(scrypt): drop the release migration and scope cooldown stamps to one quarantine episode

Review follow-ups:
- the data migration for order 127664 is removed: the order was released
  via the admin endpoint on 2026-07-28 (row is Failed), so the status
  compare-and-set makes the migration a guaranteed no-op — and the only
  state it could ever match again is a blockConfirmedOrder re-quarantine,
  which must never be flipped to Failed
- a cooldown stamp now lives for exactly one quarantine episode: it is
  cleared when the order's exit write lands (leaveQuarantine), so an
  order quarantined anew gets its first venue lookup immediately instead
  of inheriting up to 30 minutes of old wait
- two mutation-killing tests: the cooldown measures from the END of a
  slow lookup (a start-stamp mutant fails), and a re-quarantined order
  starts a fresh cooldown (removing the exit-write deletion fails)
…4446)

* fix(ledger): bound financial-log reads in SQL instead of after loading

LedgerMarkService paginated financial-log snapshots through windows, but
getFinancialLogs had no upper bound and no limit in SQL. Every window
re-read all rows from its start date to today - including the message
column at ~5.8 KB per row - and discarded the excess in JavaScript. In
production that filter matches 32'757 rows carrying 311 MB of JSON,
while the filter scan itself costs only 134 ms.

Push `to` and `limit` into the query and replace the window loop with a
keyset cursor. The cursor is the id of the last row of the previous
page; its `created` value is resolved in a correlated subquery instead
of round-tripping through a JS Date. log.created is timestamp(6) and
4999 of 5000 sampled production rows carry a microsecond remainder that
a Date truncates - comparing against the truncated value would
re-include the cursor row on every page and, at markPreloadMaxRows = 1,
never terminate.

A missing cursor row fails loud rather than yielding an empty page that
callers would misread as end-of-data. On overflow the probe read is
reused as the first page instead of being discarded and read again.

The returned row set is unchanged for identical parameters.

* fix(ledger): validate the preload row cap and detect a stale cursor reliably

Review follow-ups on the bounded financial-log reads.

markPreloadMaxRows comes from an env var and was never validated. At 0 the
probe read sliced to an empty first page and LIMIT 0 returned nothing, so
preload() silently built an empty mark cache while rows existed. It is now
required to be a positive integer and fails loud otherwise.

The cursor existence check ran before the main query, which left a window
in which cleanup() could delete the cursor row: the query then saw a NULL
cursor, returned no rows, and pagination read that as end-of-data. The
check now runs only after an empty result with a cursor set, which closes
that window and drops the extra round-trip on every non-empty page.

The pagination fake filtered on id alone while the query compares
(created, id) lexicographically, so a later row with a lower id would have
been dropped in the test but returned in production. It now resolves the
cursor row and compares the same way, with a test covering non-monotonic
ids.

Also bounds the cutover snapshot read at `to` in SQL rather than filtering
in JavaScript afterwards, now that the parameter exists.

* test(ledger): annotate the cutover snapshot mock return type
@TaprootFreak
TaprootFreak merged commit cd79760 into main Jul 29, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants