Skip to content

Settlements Flow

Settlement processing: approve and confirm phases, journal tagging, and async worker pattern.


Overview

AttributeValue
TriggerHTTP POST /v1/settlements/int
Handlersrc/handler/initSettlementHandler.ts
Worker handlersrc/handler/processSettlementWorkerHandler.ts (async Lambda invoke)
SQS consumersrc/handler/settlementJournalUpdaterHandler.ts
Servicesrc/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)

FieldTypeRequiredDescription
settlement_idstringyesUnique settlement identifier
entity_idstringyesBusiness ID
currency_codestringyesISO currency
type"approve" | "confirm"yesSettlement phase
fromstring (ISO date)yesPeriod start date
tostring (ISO date)yesPeriod end date
acquirerstring[]yesAcquirer(s) to settle
settlementISettlementItemRequestyesSettlement totals (gross, net, fee, iva)
routingISettlementItemRequestnoRouting fees (if applicable)
rolling_reserve_releasenumbernoAmount to release from reserve
force_decrease_rolling_reserve_releasebooleannoIf true, generates RR release journal
metadataobjectnoExtra context
descriptionstringno

State Validation (initSettlement)

Before invoking the worker, initSettlement() checks the current settlement state via DynamoDB:

  • Queries JournalsTable index process_id-index with process_id = settlement_id
  • Finds journals by category (SETTLEMENT_APPROVE, SETTLEMENT_CONFIRM)
  • Throws E0017 if already approved (approve called twice)
  • Throws E0018 if 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():

SignCategories
PositivePAYMENT, TOPUP, ROLLING_RESERVE_RELEASE
NegativeREFUND, 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 IN event to TransactionEventsQueue

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 updateItem calls
  • Sets settlement_id and modified_at on each journal
  • Returns batchItemFailures for 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 signs
  • gross_amount — sum with signs
  • fee_amount — sum (absolute)
  • rr_amount — sum
  • iva_amount — sum (absolute)

Net formula (conceptual):

Business net = gross - routing - rolling_reserve - fee + iva_correction

Optional 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() and sentToAccountPreparerByAcquirer() 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)

FieldDescription
entity_idBusiness ID
settlement_idSettlement ID
processed_journalsNumber of journals processed
net_amountTotal net
gross_amountTotal gross
fee_amountTotal fees
rr_amountTotal rolling reserve
iva_amountTotal IVA

Non-Obvious Behaviors

BehaviorDetail
Journal updates sent onceAfter business OUT step, before acquirer IN processing. Single enqueue covers all journal IDs (both IN and OUT).
Worker is asyncinitSettlement() returns immediately after invoking the worker Lambda. The caller does not wait for the worker to complete.
Acquirer settlement ID per acquirerEach acquirer gets a unique settlement_id (UUID v7) for its IN journals. This allows tracing per-acquirer settlements independently.
Pending journals queryQueries 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 updatessettlementJournalUpdater uses mergeMap(fn, 20) for high-throughput journal tagging.
Decimal precisionSettlement amounts use toMoney() and toRate() helpers (Decimal library) to avoid floating point errors.
REFUND/DISPUTE are negativeIn net calculations, refunds and chargebacks reduce the acquirer and business net. The sign is resolved by resolveJournalSign().
No confirm validationConfirm phase does not re-check journal state — it trusts that initSettlement() validated the order at the start.

Vecnet — Build Spec v0.2 · Obsidian Terminal