Appearance
Rolling Reserve Flow
Scheduled daily release of held rolling reserves, processed per business in timezone-aware windows.
Overview
| Attribute | Value |
|---|---|
| Trigger | CloudWatch cron, daily at 02:00 UTC |
| Orchestrator handler | src/handler/rollingReserveReleaseHandler.ts |
| Orchestrator service | src/service/RollingReserveReleaseOrchestratorService.ts |
| Processor handler | src/handler/rollingReserveReleaseProcessorHandler.ts (async Lambda invoke) |
| Processor service | src/service/RollingReserveReleaseProcessorService.ts |
Rolling reserves are amounts held from PAYMENT transactions for a configured number of days (hold_reserve_period). The cron identifies businesses with due reserves, and for each one invokes a processor Lambda that releases journals individually by re-routing them through the TransactionEventsQueue.
Flow Diagram
Release Window Calculation
resolveReleaseDayWindow() determines the UTC bounds for the current day in the business's local timezone:
timezone = business.timezone OR "America/Mexico_City"
today_local = baseTime converted to business timezone → date only
start_utc = today_local 00:00:00 in business timezone → UTC
end_utc = today_local 23:59:59 in business timezone → UTCbaseTime defaults to DateTime.utc() (now) but can be overridden via event.body.release_date.
Releasable Journal Criteria
hasReleasableTransactions() and getReleasableJournals() query MongoDB financesJournals:
| Condition | Value |
|---|---|
entity_id | business ID |
category | PAYMENT |
expected_reserve_release_date | within [start_utc, end_utc] |
settlement_id | null or "" (not yet settled) |
rr_amount | > 0 (converted from string to double via pipeline stage) |
hasReleasableTransactions() uses $limit: 1 for a fast existence check. getReleasableJournals() fetches all matching journals, sorted by expected_reserve_release_date ascending.
Journal Release Request
For each releasable journal, processJournalRelease() builds an IRollingReserveAccountPreparerRequest:
| Field | Value |
|---|---|
gross_amount | journal.rr_amount |
fee_amount | 0 |
iva_amount | 0 |
net_amount | journal.rr_amount |
category | ROLLING_RESERVE_RELEASE |
entity_id | business ID |
process_id | new UUID v7 |
currency_code | journal currency |
acquirer | journal acquirer |
original_journal | { id, type, fee_rule_id, acquirer, provider, payment_method_id } |
metadata.process_type | ROLLING_RESERVE_RELEASE |
metadata.journal_id | source journal ID |
metadata.original_process_id | source journal process_id |
metadata.release_date | ISO date string (local) |
metadata.release_generated_at | timestamp of this invocation |
SQS Event
The release request is sent to TransactionEventsQueue as a simulated EventBridge event:
json
{
"detail-type": "SETTLEMENTS.ROLLING_RESERVE_RELEASE",
"detail": {
"data": { "NewImage": { ...request } }
}
}MessageGroupId = entity_id (FIFO ordering per business).
This triggers OrchestratorService.processBySettlement() → accountingPreparer() → accountingRollingReservePreparer(), which creates a ROLLING_RESERVE_RELEASE journal linked to the original PAYMENT journal.
Processor Event Structure (IRollingReserveReleaseProcessorEvent)
| Field | Type | Description |
|---|---|---|
entity_id | string | Business ID |
timezone | string | Business timezone |
release_date | string | Local date (ISO) |
start_utc | string | Window start (ISO datetime) |
end_utc | string | Window end (ISO datetime) |
Concurrency Model
| Level | Concurrency | Reason |
|---|---|---|
| Orchestrator → businesses | 5 | Limit MongoDB queries and Lambda invocations |
| Processor → journals per business | 5 | Limit SQS message throughput per entity |
Errors per journal are caught and collected; one failed journal does not stop others.
Non-Obvious Behaviors
| Behavior | Detail |
|---|---|
| Process date = NOW | Unlike transactions, the ROLLING_RESERVE_RELEASE journal uses nowMillis() as process_date, not the original payment date. The release event is a new accounting event in current time. |
| Original journal preserved | original_journal metadata captures the source PAYMENT journal's id, type, acquirer, provider, payment_method_id. Used by accountingRollingReservePreparer() to link the journals via related_journal_id. |
| rr_amount stored as string | MongoDB financesFeeRules stores rr_amount as string for Decimal precision. The pipeline adds a $addFields stage with $toDouble before filtering > 0. |
| settlement_id null check | Only journals not yet settled (settlement_id = null or "") are eligible. Already-settled journals have had their reserve included in the settlement flow. |
| Default timezone | America/Mexico_City is used when business.timezone is not set. |
| Processor function optional | If ROLLING_RESERVE_RELEASE_PROCESSOR_FUNCTION_NAME env var is not set, the orchestrator returns early with 0 processed merchants. |
| Existence check first | hasReleasableTransactions() uses $limit: 1 to avoid querying all journals just to decide whether to invoke a Lambda. |
| Sent through TransactionEventsQueue | The release is processed by the same transactionOrchestratorHandler as regular transactions, via processBySettlement(). No separate Lambda for accounting. |