Where a payment becomes an accounting fact.
Owns the payment state machine, a double-entry ledger kept independently of the acquirer’s, the saga that compensates for unknown outcomes, a transactional outbox, and reconciliation.
Authorisation is a hold and posts nothing. Capture is the money. Conflating the two is the most common modelling error in payments software.
Transactions
6
every lifecycle state
Captured
3
money actually moved
Ledger postings
3
9 signed entries
Ledger balance
balanced
every posting sums to zero
Chart of accounts
Positive is a debit, negative a credit. One signed column makes “does this balance” a single SUM.
REVENUE:PROCESSING_FEES$-6.26ASSETS:ACQUIRER_RECEIVABLE$375.16ASSETS:PSP_RECEIVABLE$86.12LIABILITIES:MERCHANT_PAYABLE$-455.02- Sum$0.00
Endpoints
- POST
/api/v1/paymentsRecords PENDING before contacting the acquirer, so a crash cannot orphan a hold. - POST
/api/v1/payments/:id/captureThe financial event. Both ledgers post here. - POST
/api/v1/payments/:id/refundA new mirrored transaction, never an edit. - POST
/api/v1/external-captureStripe-rail captures land here, booked to PSP_RECEIVABLE. - POST
/api/v1/reconcilePer line, scoped to one counterparty. - GET
/api/v1/trace/:traceIdThis service’s account of one payment.
The patterns, and what breaks without them
0 reconciliation runs recorded.
Transactional outbox
Commit-then-publish loses events on a crash; publish-then-commit emits events for things that never happened. Writing the intent in the same transaction turns an impossible atomicity problem into ordinary at-least-once delivery.
Saga with compensation
A timeout is not a failure. It is an unknown outcome. Retrying could place a second hold, so the processor sends a reversal instead, which is safe whether or not a hold exists.
Reversal is a new entry
Never a delete. Deleting a posting destroys the audit trail; the correct record is “this happened, then it was undone”.
Reconciliation per counterparty
Rail A lines are compared against AKM Bank, rail B against Stripe. Pointing one file at the other’s captures reports every one as a break.