[BTAPI-73] Fix Wealthsimple sync flip-flop on settling transactions #90

Merged
chris merged 3 commits from bug/BTAPI-73 into main 2026-09-03 16:54:41 -06:00
Owner

Ticket

BTAPI-73 Wealthsimple sync flip-flops a settling transaction's amount, sending duplicate revision pushes

Summary

  • Dedupe same-wsId activities within a single fetch before the upsert loop, preferring the settled node over a pending duplicate (and the later occurredAt when both agree on pendingness) — this is the fix for the actual bug: two coexisting feed nodes for one purchase no longer let "whichever is later in array order" decide the stored amount/pending flag.
  • Refuse to revert an already-settled transaction back to pending when a stale pending activity for it shows up in a later, separate fetch window (belt-and-suspenders for duplicates that don't land in the same tick).
  • Log a warning whenever the dedupe actually collapses two activities, carrying enough detail (wsId, canonicalId, status, amount, merchant for both the kept and dropped side) to distinguish a routine pending/posted collapse from a genuine two-purchase collision, should deriveExternalId ever produce one.
  • Test coverage: a same-tick pending/posted collapse in both array orders, an occurredAt tiebreak independent of array position, a 3+-node collapse, the stale-settled-guard (now pinned against merchant/canonicalId/date, not just amount), and the collapse log itself.

Follow-up (not part of this PR)

  • A manual, post-deploy data correction for the specific Walmart.Ca transaction that originally exposed this bug — must run only after this fix is deployed, otherwise the next sync tick just flips it again:
    db.transactions.updateOne(
        { externalId: 'card-activity-00000000527027808334-VI-00-0356242693135191-EKG7YF' },
        { $set: { amount: 342.56, syncedAmount: 342.56, pending: false } },
    )
    
  • A reviewer flagged a narrow residual race: the settled-guard reads a per-tick DB snapshot, so two genuinely concurrent engine instances (e.g. the manual ws:sync CLI racing the automatic cron/doorbell engine) could theoretically still slip a stale pending activity through in the snapshot-to-write window. This would need new DB-layer conditional-update plumbing beyond this ticket's scope; documented as a known, vanishingly-unlikely residual limitation for this single-user app rather than fixed here.
## Ticket [BTAPI-73](http://192.168.2.100:7123/home/browse/BTAPI-73/) Wealthsimple sync flip-flops a settling transaction's amount, sending duplicate revision pushes ## Summary - Dedupe same-`wsId` activities within a single fetch before the upsert loop, preferring the settled node over a pending duplicate (and the later `occurredAt` when both agree on pendingness) — this is the fix for the actual bug: two coexisting feed nodes for one purchase no longer let "whichever is later in array order" decide the stored amount/pending flag. - Refuse to revert an already-settled transaction back to pending when a stale pending activity for it shows up in a later, separate fetch window (belt-and-suspenders for duplicates that don't land in the same tick). - Log a warning whenever the dedupe actually collapses two activities, carrying enough detail (`wsId`, `canonicalId`, `status`, `amount`, `merchant` for both the kept and dropped side) to distinguish a routine pending/posted collapse from a genuine two-purchase collision, should `deriveExternalId` ever produce one. - Test coverage: a same-tick pending/posted collapse in both array orders, an `occurredAt` tiebreak independent of array position, a 3+-node collapse, the stale-settled-guard (now pinned against `merchant`/`canonicalId`/`date`, not just `amount`), and the collapse log itself. ## Follow-up (not part of this PR) - A manual, post-deploy data correction for the specific Walmart.Ca transaction that originally exposed this bug — must run only after this fix is deployed, otherwise the next sync tick just flips it again: ```js db.transactions.updateOne( { externalId: 'card-activity-00000000527027808334-VI-00-0356242693135191-EKG7YF' }, { $set: { amount: 342.56, syncedAmount: 342.56, pending: false } }, ) ``` - A reviewer flagged a narrow residual race: the settled-guard reads a per-tick DB snapshot, so two genuinely concurrent engine instances (e.g. the manual `ws:sync` CLI racing the automatic cron/doorbell engine) could theoretically still slip a stale pending activity through in the snapshot-to-write window. This would need new DB-layer conditional-update plumbing beyond this ticket's scope; documented as a known, vanishingly-unlikely residual limitation for this single-user app rather than fixed here.
Pin the dedupe preference logic with tests independent of array order
(mutation-verified gap), fully pin the stale-activity suppression guard
(merchant/canonicalId/date, not just amount/pending), correct two
misleading comments, tighten dedupeByWsId's return type to readonly,
and log a warning whenever the dedupe actually collapses two activities.
BTAPI-73: Polish the collapse-logging code from the last review round
All checks were successful
server / check (pull_request) Successful in 34s
plane-sync / sync (pull_request) Successful in 1s
4d0a7dae6d
Collapse the two near-identical log.warn call sites in dedupeByWsId into
one, add amount/merchant to the payload so a genuine two-purchase
collision is actually distinguishable from a pending/posted pair, fix the
message wording for a 3+-node collapse, and add a test asserting the log
fires with the correct kept/dropped identification.
chris merged commit 179e496978 into main 2026-09-03 16:54:41 -06:00
chris deleted branch bug/BTAPI-73 2026-09-03 16:54:41 -06:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chris/budget-tracker!90
No description provided.