Summary
Card apply decided the open-fee top-up from our own card ledger alone. KeySecure webhooks don't reach staging yet, so the first card's Open_card fee was never posted:
- our ledger says 2.00;
- KeySecure's cardholder balance is 0;
- the second apply skipped the top-up and got 3008, so the card went FAILED and the app got a 502.
Apply now also reads the KeySecure balance and uses whichever figure is lower.
-
Where it reads: inside the existing per-account advisory-locked transaction,
apply()readsaccount.infofor the cardholder (sum ofusdBalance). -
Top-up size:
openCost - min(ledger spendable, provider balance), rounded up to 2 dp. - Ledger ahead of KeySecure: if the ledger exceeds the KeySecure balance by more than 0.01, apply logs a WARN with numbers only. Reconciliation already alerts on that drift.
- Provider read fails: it maps like the package fee read (503 not configured, 502 provider refused, transport errors pass through). Nothing is created.
- Sweeper path: unchanged.
Testing
-
yarn jest src/modules/card-account: 226 pass. Includes the staging case (ledger 2, provider 0, so it tops up 2.00), the reverse case, the no-top-up case, a multi-item sum, and error mapping. -
RUN_DB_TESTS=1on a disposable DB: 233 pass.yarn buildand eslint are clean. - Mutation check: replacing
minwith the ledger value fails the staging test.
Notes
- Lock held during the KeySecure call: the DB transaction and its connection stay open while KeySecure answers, up to about 35s worst case on retries. That's acceptable: apply is rare and the lock is per card account.
- Concurrent applies (existing, out of scope): two applies at once can still count the same money, because KeySecure takes the fee after the lock is released. One of them gets 3008.
- Root cause still open: KeySecure webhooks must be enabled so Open_card fees and swipes get posted.