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/ CRfee_revenue.CARD_USD; before, they went tocapital.CARD_USD. A later lower fee reverses throughfee_revenue. Purchase amounts, refunds and reversals are unchanged and stay oncapital. -
Contract fees are booked as a cost. On an Open_card or Manage_fee Success, the configured contract fee posts DR
reserve.CARD_USD/ CRcard_provider_cost.CARD_USD.- It posts once per consumeNo, guarded by the new
card_consumptions.posted_provider_costunder the existing row lock. - KeySecure reserve records carry no card or consume reference, so the fee comes from config rather than the reserve list.
- It posts once per consumeNo, guarded by the new
-
New config:
-
KEYSECURE_CONTRACT_OPEN_CARD_FEE_USDandKEYSECURE_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_costsystem account for CARD_USD. It is not inSYSTEM_ACCOUNT_TYPES. - Adds the
posted_provider_costcolumn. - The down migration refuses if the account has a balance or ledger entries.
- Adds the
-
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 --runInBandover 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 buildpasses. - Opus review gate: approved in round 1.
Deploy / follow-ups
-
Staging: add
KEYSECURE_CONTRACT_OPEN_CARD_FEE_USD=2to 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_consumptionsrow with itsposted_*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_USDis below the fee, the webhook fails and replays until treasury funds the reserve.