Appearance
Settlements Flow
Settlement processing: approve and confirm phases, journal tagging, and async worker pattern.
Overview
| Attribute | Value |
|---|---|
| Trigger | HTTP POST /v1/settlements/int |
| Handler | src/handler/initSettlementHandler.ts |
| Worker handler | src/handler/processSettlementWorkerHandler.ts (async Lambda invoke) |
| SQS consumer | src/handler/settlementJournalUpdaterHandler.ts |
| Service | src/service/SettlementService.ts |
A settlement runs in two phases: approve (identify and process pending journals) and confirm (finalize disbursement). Each phase is triggered independently via the same endpoint with a different type field.
Flow Diagram
API Endpoint
POST /v1/settlements/int
Request Schema (ISettlementRequest)
| Field | Type | Required | Description |
|---|---|---|---|
settlement_id | string | yes | Unique settlement identifier |
entity_id | string | yes | Business ID |
currency_code | string | yes | ISO currency |
type | "approve" | "confirm" | yes | Settlement phase |
from | string (ISO date) | yes | Period start date |
to | string (ISO date) | yes | Period end date |
acquirer | string[] | yes | Acquirer(s) to settle |
settlement | ISettlementItemRequest | yes | Settlement totals (gross, net, fee, iva) |
routing | ISettlementItemRequest | no | Routing fees (if applicable) |
rolling_reserve_release | number | no | Amount to release from reserve |
force_decrease_rolling_reserve_release | boolean | no | If true, generates RR release journal |
metadata | object | no | Extra context |
description | string | no |
State Validation (initSettlement)
Before invoking the worker, initSettlement() checks the current settlement state via DynamoDB:
- Queries
JournalsTableindexprocess_id-indexwithprocess_id = settlement_id - Finds journals by category (
SETTLEMENT_APPROVE,SETTLEMENT_CONFIRM) - Throws
E0017if already approved (approve called twice) - Throws
E0018if attempting to confirm before any approval journal exists
Approve Phase
1. getPendingJournals
DynamoDB query:
- Index:
entity_id-created_at-index - Key:
entity_id = entity_id - Range:
created_at BETWEEN from AND to - Filter:
settlement_id = null OR settlement_id = "" - Optional filter:
acquirer IN acquirer[](if provided)
Returns all journals not yet tagged with a settlement_id.
2. sentToAccountPreparerByBusiness (OUT)
Calculates business totals using calculateBusinessNetByCategory():
| Sign | Categories |
|---|---|
| Positive | PAYMENT, TOPUP, ROLLING_RESERVE_RELEASE |
| Negative | REFUND, DISPUTE_IN_REVIEW, DISPUTE_WON, WITHDRAWAL, ROUTING |
Totals: net_amount, gross_amount, fee_amount, rr_amount, iva_amount.
Sends SETTLEMENT_APPROVE OUT event to TransactionEventsQueue (EventBridge).
3. sentToAccountPreparerByAcquirer (IN, per acquirer)
Groups IN journals by acquirer. For each acquirer:
calculateNetByCategory()— same sign logic as above- Generates a new
settlement_id(UUID v7) per acquirer - Sends
SETTLEMENT_APPROVE INevent toTransactionEventsQueue
4. enqueueSettlementJournalUpdates
Journal IDs are batched (100 per message) and sent to SettlementJournalsQueue. Called once, after business OUT and before acquirer IN processing. The enqueue is idempotent via settlement_id.
5. updateJournals (SQS consumer)
settlementJournalUpdaterHandler processes each SQS message:
- Runs up to 20 concurrent DynamoDB
updateItemcalls - Sets
settlement_idandmodified_aton each journal - Returns
batchItemFailuresfor failed items (SQS retry)
Confirm Phase
Confirm only calls sentToAccountPreparerByBusiness() with type = SETTLEMENT_CONFIRM:
- No journal query
- No acquirer processing
- No additional SQS enqueue
Net Calculation Formulas
calculateNetByCategory (acquirer-side)
net = journals.reduce((acc, journal) => {
sign = positiveCategories.includes(journal.category) ? +1 : -1
return acc + sign × journal.net_amount
})Positive categories: PAYMENT, TOPUP, ROLLING_RESERVE_RELEASE Negative categories: REFUND, DISPUTE_IN_REVIEW, DISPUTE_WON, WITHDRAWAL
calculateBusinessNetByCategory
Groups journals and computes:
net_amount— sum with signsgross_amount— sum with signsfee_amount— sum (absolute)rr_amount— sumiva_amount— sum (absolute)
Net formula (conceptual):
Business net = gross - routing - rolling_reserve - fee + iva_correctionOptional Sub-flows
Routing (routing field present)
Sends a ROUTING category event to the orchestrator before the business settle step. Creates a routing fee journal via accountingRoutingPreparer().
Rolling Reserve Release (force_decrease_rolling_reserve_release = true)
Sends a ROLLING_RESERVE_RELEASE event. Requires rolling_reserve_release amount set. Creates a reserve release journal via accountingRollingReservePreparer().
Bulk Settlements (Phase 6)
BulkReprocessSettlementsService.processSettlements() (called by bulkSettlementsHandler) reuses the same account preparer flow for historical settlement periods stored in the bulk record:
- Iterates over
settlement_periods[]from the BulkReprocessRecord - For each period: calls approve + confirm via the same
sentToAccountPreparerByBusiness()andsentToAccountPreparerByAcquirer()methods - Returns
{ count, failed }counts
See docs/bulk/bulk-reprocess.md (Tonder source — not included in this package) for the full bulk flow.
Response Structure (ISettlementResponse)
| Field | Description |
|---|---|
entity_id | Business ID |
settlement_id | Settlement ID |
processed_journals | Number of journals processed |
net_amount | Total net |
gross_amount | Total gross |
fee_amount | Total fees |
rr_amount | Total rolling reserve |
iva_amount | Total IVA |
Non-Obvious Behaviors
| Behavior | Detail |
|---|---|
| Journal updates sent once | After business OUT step, before acquirer IN processing. Single enqueue covers all journal IDs (both IN and OUT). |
| Worker is async | initSettlement() returns immediately after invoking the worker Lambda. The caller does not wait for the worker to complete. |
| Acquirer settlement ID per acquirer | Each acquirer gets a unique settlement_id (UUID v7) for its IN journals. This allows tracing per-acquirer settlements independently. |
| Pending journals query | Queries entity_id-created_at-index — journals are indexed by creation time, not transaction time. Filters on settlement_id = null to find untagged journals. |
| 20 concurrent DynamoDB updates | settlementJournalUpdater uses mergeMap(fn, 20) for high-throughput journal tagging. |
| Decimal precision | Settlement amounts use toMoney() and toRate() helpers (Decimal library) to avoid floating point errors. |
| REFUND/DISPUTE are negative | In net calculations, refunds and chargebacks reduce the acquirer and business net. The sign is resolved by resolveJournalSign(). |
| No confirm validation | Confirm phase does not re-check journal state — it trusts that initSettlement() validated the order at the start. |