Skip to content
GitLab
Projects Groups Topics Snippets
  • /
  • Help
    • Help
    • Support
    • Community forum
    • Submit feedback
    • Contribute to GitLab
  • Register
  • Sign in
  • A Ahad Mobile
  • Project information
    • Project information
    • Activity
    • Labels
    • Members
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributor statistics
    • Graph
    • Compare revisions
  • Issues 0
    • Issues 0
    • List
    • Boards
    • Service Desk
    • Milestones
  • Merge requests 0
    • Merge requests 0
  • CI/CD
    • CI/CD
    • Pipelines
    • Jobs
    • Schedules
  • Deployments
    • Deployments
    • Environments
    • Releases
  • Packages and registries
    • Packages and registries
    • Package Registry
    • Terraform modules
  • Monitor
    • Monitor
    • Incidents
  • Analytics
    • Analytics
    • Value stream
    • CI/CD
    • Repository
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Activity
  • Graph
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
Collapse sidebar
  • holatech
  • Ahad Mobile
  • Merge requests
  • !91

fix(card-account): fund the KeySecure open fee before card apply

  • Review changes

  • Download
  • Patches
  • Plain diff
Merged Khanh Pham requested to merge fix/keysecure-card-open-fee into develop Oct 02, 2026
  • Overview 0
  • Commits 8
  • Pipelines 1
  • Changes 22

Summary

KeySecure charges the card package open fee to the cardholder account balance. Issuing a card with an empty card balance therefore always returned 3008 ("insufficient balance"), and the card stayed APPLYING forever. This MR funds the fee from the user's USD wallet before card/apply. It also fixes two related problems found while testing on the sandbox.

  • Fund before apply (card-issuance.service.ts):
    • If the card's provider-spendable balance (balance − pending swipes − PENDING In transfers) is below the package open_cost, apply tops up the shortfall from the USD wallet. The top-up goes through the existing deposit flow (card_transfers In, deposit fee waived), in the same DB transaction as the card row.
    • card/apply is only called once that transfer is APPLIED.
    • Concurrent applies on the same card account are serialised by an advisory lock.
    • A conditional claim on submitted_at makes sure the request path and the sweeper never both submit.
  • Sweeper (reconcileApplying):
    • For an unsubmitted card, it waits while the top-up is PENDING.
    • If the top-up FAILED, it fails the card. The deposit's own onFailed has already returned the wallet money.
    • Otherwise it claims the card and submits.
  • open_cost: read from /card/package/list (fee_detail.open_cost) and cached for 10 minutes. A missing key counts as 0.
  • 3008: now a refusal. The card is FAILED and the internal issue fee refunded. If apply is refused after the top-up landed, the money stays on the card balance.
  • Wallet too low: returns 422 INSUFFICIENT_BALANCE and creates no card row.
  • Migration 1790200000000: adds keysecure_cards.funding_transfer_id and submitted_at. Existing rows are backfilled as submitted.
  • KeySecure list windows: list endpoints compare end_time by date only and exclude the end date, so today's rows were missing. listParams now rounds end_time up to the next 00:00 UTC. This fixes the stale reserve balance and the same-day APPLYING card lookup.
  • Reconciliation CARD: now SUM(card_accounts) + reserve.CARD_USD + fee_revenue.CARD_USD − pending swipes == reserve fund. The cardholder account.info sum is dropped. On the sandbox, a transfer In does not move the reserve fund; only real spend (fees, swipes) does.

Testing

  • yarn jest src/modules/card-account src/modules/treasury src/modules/keysecure src/modules/transactions: all pass.
  • RUN_DB_TESTS=1 on a disposable local DB: 29/29 card-account and treasury suites pass, including the card-flow invariant spec with an open-fee case.
  • Migration run, revert and run again on that DB: OK.
  • yarn build and eslint on the changed files are clean. 4 eslint errors in reconciliation.service.spec.ts predate this branch.
  • Not yet checked end to end on the sandbox. That needs this branch on staging.

Notes / follow-ups

  • Two applies at once: the second can still get 3008 until the first card's Open_card fee settles. It ends FAILED and nothing is lost.
  • Timeout while reading the package fee: returns 500. Nothing has been created, so a retry is safe.
  • Two comments in reconciliation.service.ts to tidy: one calls the pending-swipe assumption verified when it isn't yet, and one still mentions account.info.
  • After deploy:
    • Today's manual sandbox test (5 USD transfer In, 2 USD fee) is not in our books, so CARD will show a −2 difference until it is adjusted.
    • Cards failed with APPLY_TIMEOUT should be checked against /card/list. The old date filter may have failed cards that are live at KeySecure.

🤖 Generated with Claude Code

Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: fix/keysecure-card-open-fee