Skip to content

Card settlement — the model (AngelPay reconciliation, Option X)

Purpose: the domain truth for how a card sale’s money flows from the terminal to a withdrawable merchant balance — what the system must uphold, independent of how it’s built. Audience: AI agents (and the humans reviewing them) building the CardSettlement sub-project. Status: as of 2026-09-21 — design-complete, ready to draft; PO-approved 2026-09-20. Source decision log: ../../backlog/ideation/api-card-settlement-angelpay-reconciliation.md. Operator flow: ../runbooks/card-settlement-reconciliation.md. Related: pos-domain-model.md · reporting.md · runtime-settings.md · terminales-api/src/core/money.ts (MINOR_SCALE).

A card charge on the N86 (the AngelPay SDK) does not land in Axios’s account at sale time — the money lands in AngelPay’s account. AngelPay settles it to the Axios affiliation (9613149) at T+1, automatically, paying net (it deducts its agreed commission before paying Axios); Fri–Sun settle Monday, cutoff 10:59 pm. Axios then pays merchants via liquidaciones (money-out). So at the moment a merchant sees a card sale credited to their wallet, that money is virtual — promised by AngelPay, not yet confirmed to Axios. This model records which card money is virtual vs confirmed, without changing what the merchant can already do with a fresh sale.

The “bank statement” the system imports to reconcile is the AngelPay movements export (an xlsx from the AngelPay portal), not a bank CSV. The siafrac-style bank-transfer / per-merchant-reference deposit reconciliation is a separate later wave (see Deferred, below).

The core rule — Option X (PO, 2026-09-20)

Section titled “The core rule — Option X (PO, 2026-09-20)”

Keep the at-sale gross wallet credit; only withdrawals to a bank require settlement. This is the live path today and it does not change:

  • A card sale credits the merchant wallet its gross amount at sale time, AVAILABLE, exactly as it does now.
  • That balance stays immediately usable for VAS, merchant→merchant transfers, and top-ups — those check the gross availableBalance snapshot and are unaffected by settlement.
  • Only a withdrawal to a bank (a liquidación / money-out) is gated on confirmed money — on settlement_reports.availableAmount (see the three money figures below).

A per-merchant X/Y runtime toggle (some merchants gross-at-sale, some settle-first) was rejected — it forks ledger semantics. There is one path: gross at sale, settlement gates only the withdrawal.

The AngelPay import is the source of truth for settlement

Section titled “The AngelPay import is the source of truth for settlement”

The nightly rollup that computed settlement reports from terminales’ own records (terminales-api/src/wallet/settlements/settlement-rollup.scheduler.ts, crontab 30 6 * * *) is disabled, not deleted — gated behind a new realm setting settlement.source with values angelpay_import | computed_rollup, default angelpay_import. The rollup is kept as a fallback for a future PSP that cannot provide a settlement export. See runtime-settings.md for the resolver; the on-demand fallback and the kill switch are in the reconciliation runbook.

The import is a second writer of availableAmount (the first is applyReportAvailable() in settlement-requests.service.ts); the two must coexist, selected by settlement.source.

A card sale carries three distinct amounts. Conflating them is the classic settlement bug; the model keeps all three:

Figure What it is Source Used for
Gross The full sale amount, credited to the wallet at sale time AngelPay Monto The at-sale wallet credit (Option X) — the availableBalance snapshot VAS/transfers/top-ups spend against
AngelPay net Gross − AngelPay’s commission (what AngelPay actually deposits to Axios) AngelPay Total Depositar The Axios↔AngelPay commissions report (what the processor cost us)
Merchant net (withdrawable) Gross of confirmed sales − Axios’s effective-dated commission Computed at report time settlement_reports.availableAmount — what the merchant may withdraw

Note the two commissions are different: AngelPay’s (baked into Total Depositar) and Axios’s (the effective-dated rule, below). Axios may absorb AngelPay’s and/or waive its own.

settlementStatus — a new, orthogonal column

Section titled “settlementStatus — a new, orthogonal column”

A new settlementStatus on the card deposit tracks confirmation, independent of the existing status:

  • Values: pending_settlement → settled | rejected.
  • It does not overload status. status controls usability and stays AVAILABLE under Option X (the money is spendable from the moment of sale). settlementStatus answers a different question — “is this card money virtual or confirmed?”
  • Target: the deposits table (terminales-api/src/wallet/deposits/deposits.constants.ts; generated types appwrite-row-types.generated.ts; schema terminales-api/infra/appwrite/appwrite.config.json). Channel-E card deposits are written AVAILABLE today in deposits.service.ts register(); salesOrderId is the link back to the sale.

The import flips an existing card deposit’s settlementStatus — under Option X the deposit already exists from the sale, so the import never creates or credits.

unsettledBalance — how much of the balance is virtual

Section titled “unsettledBalance — how much of the balance is virtual”

unsettledBalance = Σ of a merchant’s pending_settlement card deposits, computed from the deposits table (indexed by merchantId), exposed for reporting. It answers “how much of this balance is still virtual right now” — the credit-side view. (It does not answer “did this specific debit spend virtual money” — that per-debit attribution is deferred, below.)

Effective-dated commission (Axios’s own)

Section titled “Effective-dated commission (Axios’s own)”

Axios’s commission is a set of effective-dated rules (datetime ranges), global with a per-merchant cascade, and each rule may be $0.00 (Axios can absorb AngelPay’s commission and/or waive its own). It applies only when a settlement report’s withdrawable amount is computed — never in the cart. No commission is shown at the POS (the cart is unchanged). Today’s single rate resolves via RuntimeSettingsService.inspect (merchant → realm → default) in settlement-rollup.scheduler.ts; the effective-dated rules generalise that.

  • Per-ROW idempotency, not per-file. Operators re-export and overlap date ranges, so the AngelPay ID Transacción is the dedup key. It is stored as a settlement-reference column on the deposit — not as the deposit idempotencyId (that field is the tender clientId that drives the wallet-credit dedup, a separate concern).
  • Join key — AngelPay row (Autorización | Referencia/ID Transacción | Folio) → payment_intents (authCode / integratorReference / folio) → salesOrderId → the CARD_TERMINAL deposit. A multi-tender sale shares one salesOrderId, so disambiguate by amount. Terminal serial → terminals.serialNumber → storeTeamId → merchant.

All money is integer minor units, ×10000 (terminales-api/src/core/money.ts MINOR_SCALE = 10_000) — see AI_CONTEXT hard-rule #9. Commission math uses one rounding rule (half-up) and the result is compared against AngelPay’s reported figure.

The operator screens and i18n say “cobros con tarjeta” / “procesador de pagos” — never the brand. “AngelPay” appears in code, docs, and column mappings only, never in merchant- or operator-facing copy.

  • Rejected / unmatched already-credited sale → flag it in the exceptions queue and leave the wallet credit in place for an operator to decide. No auto-reversal in MVP (see Deferred). Under Option X the money was already spendable; clawing it back is a deliberate later “money-backwards” ticket.
  • Overlapping / re-exported rows → deduped by ID Transacción (per-row idempotency); a row already settled is a no-op.
  • settlement.source = computed_rollup → the import is bypassed; reports come from the nightly rollup. This is the kill switch.

These were explicitly deferred by the PO on 2026-09-20 and are recorded here so they are not lost. The runbook repeats them; ../progress/open-questions-and-todos.md is the live tracker.

  • Per-debit “used virtual money” flag. Exact per-transaction attribution — stamping a VAS/transfer debit with how much of it was funded by pending_settlement money — is a follow-up. This wave ships only the credit-side settlementStatus + unsettledBalance, which answer “which money is virtual,” not “this specific debit spent virtual money.” Revisit when per-debit reporting is needed.
  • Bank-transfer wallet-deposit reconciliation (the siafrac model: per-merchant reference, staged bank-statement import). This was the original sub-project 4, deferred by “scope A” on 2026-09-20. Its design bullets remain in backlog/ideation/multi-platform-roles-admin-operator.md.
  • Auto-reversal of a rejected already-credited card sale. MVP flags the exception only; reversal is a later deliberate money-backwards ticket.