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
  • !96

fix(card): book user card fees as revenue and contract fees as provider cost

  • Review changes

  • Download
  • Patches
  • Plain diff
Merged Khanh Pham requested to merge fix/card-fee-revenue-provider-cost into develop Oct 05, 2026
  • Overview 0
  • Commits 4
  • Pipelines 1
  • Changes 12

Summary

KeySecure charges two different card fees. The user pays the card plan fee from their cardholder balance. We pay the contract fee from our reserve fund. Our books treated them as the same money, which only worked because both were 2 USD on the sandbox. A different user price in prod would drift the CARD reconciliation on every card, and we would never book the revenue.

  • User card fees are booked as revenue. This covers Open_card, Manage_fee, and the fee part of a swipe. They now post DR normal.CARD_USD / CR fee_revenue.CARD_USD; before, they went to capital.CARD_USD. A later lower fee reverses through fee_revenue. Purchase amounts, refunds and reversals are unchanged and stay on capital.
  • Contract fees are booked as a cost. On an Open_card or Manage_fee Success, the configured contract fee posts DR reserve.CARD_USD / CR card_provider_cost.CARD_USD.
    • It posts once per consumeNo, guarded by the new card_consumptions.posted_provider_cost under the existing row lock.
    • KeySecure reserve records carry no card or consume reference, so the fee comes from config rather than the reserve list.
  • New config:
    • KEYSECURE_CONTRACT_OPEN_CARD_FEE_USD and KEYSECURE_CONTRACT_MANAGE_FEE_USD, both non-negative decimals.
    • Default 0. A malformed value fails at boot.
    • A contract fee that is not configured shows up as negative CARD drift instead of being invented.
  • Migration 1790300000000:
    • Adds the card_provider_cost system account for CARD_USD. It is not in SYSTEM_ACCOUNT_TYPES.
    • Adds the posted_provider_cost column.
    • The down migration refuses if the account has a balance or ledger entries.
  • Reconciliation: the CARD formula is unchanged (SUM(card_accounts) + reserve.CARD_USD + fee_revenue.CARD_USD − pending == reserve fund). It now holds when the user fee and the contract fee differ. Only the doc comment changed.

Testing

  • RUN_DB_TESTS=1 DB_NAME=moanapay_test jest --runInBand over treasury, card-account, keysecure/config and transactions: 45 suites, 462 tests passed.
  • New postgres cases:
    • Open_card with user fee 5 and contract fee 2: card −5, fee_revenue +5, reserve −2, provider cost +2, and CARD level OK.
    • Replays and concurrent deliveries post the cost once.
    • A swipe splits into a fee (fee_revenue) and an amount (capital).
    • A contract fee that is not configured reports CARD drift −2.
  • Every new assertion was mutation-checked.
  • Migration up, down, up on moanapay_test.
  • eslint is clean on changed files, and yarn build passes.
  • Opus review gate: approved in round 1.

Deploy / follow-ups

  • Staging: add KEYSECURE_CONTRACT_OPEN_CARD_FEE_USD=2 to the app env, matching the sandbox reserve deduction. Prod values come from the signed contract and are still to be confirmed.
  • Staging card 8242: its Open_card fee predates working webhooks. After deploy, a one-off reviewed SQL script backfills it with the new postings, inserting the card_consumptions row with its posted_* fields set.
  • Known limits, out of scope:
    • No per-transaction contract fee, because its structure is unknown; it will surface as CARD drift.
    • No contract monthly fee for cards whose plan has no Manage_fee.
    • A KeySecure refund of an issuance fee would go through capital.
    • The cost leg keeps the reserve balance guard. If reserve.CARD_USD is below the fee, the webhook fails and replays until treasury funds the reserve.

🤖 Generated with Claude Code

Assignee
Assign to
Reviewers
Request review from
Time tracking
Source branch: fix/card-fee-revenue-provider-cost