Skip to content

Rolling Reserve Flow

Scheduled daily release of held rolling reserves, processed per business in timezone-aware windows.


Overview

AttributeValue
TriggerCloudWatch cron, daily at 02:00 UTC
Orchestrator handlersrc/handler/rollingReserveReleaseHandler.ts
Orchestrator servicesrc/service/RollingReserveReleaseOrchestratorService.ts
Processor handlersrc/handler/rollingReserveReleaseProcessorHandler.ts (async Lambda invoke)
Processor servicesrc/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 → UTC

baseTime defaults to DateTime.utc() (now) but can be overridden via event.body.release_date.


Releasable Journal Criteria

hasReleasableTransactions() and getReleasableJournals() query MongoDB financesJournals:

ConditionValue
entity_idbusiness ID
categoryPAYMENT
expected_reserve_release_datewithin [start_utc, end_utc]
settlement_idnull 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:

FieldValue
gross_amountjournal.rr_amount
fee_amount0
iva_amount0
net_amountjournal.rr_amount
categoryROLLING_RESERVE_RELEASE
entity_idbusiness ID
process_idnew UUID v7
currency_codejournal currency
acquirerjournal acquirer
original_journal{ id, type, fee_rule_id, acquirer, provider, payment_method_id }
metadata.process_typeROLLING_RESERVE_RELEASE
metadata.journal_idsource journal ID
metadata.original_process_idsource journal process_id
metadata.release_dateISO date string (local)
metadata.release_generated_attimestamp 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)

FieldTypeDescription
entity_idstringBusiness ID
timezonestringBusiness timezone
release_datestringLocal date (ISO)
start_utcstringWindow start (ISO datetime)
end_utcstringWindow end (ISO datetime)

Concurrency Model

LevelConcurrencyReason
Orchestrator → businesses5Limit MongoDB queries and Lambda invocations
Processor → journals per business5Limit SQS message throughput per entity

Errors per journal are caught and collected; one failed journal does not stop others.


Non-Obvious Behaviors

BehaviorDetail
Process date = NOWUnlike 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 preservedoriginal_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 stringMongoDB financesFeeRules stores rr_amount as string for Decimal precision. The pipeline adds a $addFields stage with $toDouble before filtering > 0.
settlement_id null checkOnly journals not yet settled (settlement_id = null or "") are eligible. Already-settled journals have had their reserve included in the settlement flow.
Default timezoneAmerica/Mexico_City is used when business.timezone is not set.
Processor function optionalIf ROLLING_RESERVE_RELEASE_PROCESSOR_FUNCTION_NAME env var is not set, the orchestrator returns early with 0 processed merchants.
Existence check firsthasReleasableTransactions() uses $limit: 1 to avoid querying all journals just to decide whether to invoke a Lambda.
Sent through TransactionEventsQueueThe release is processed by the same transactionOrchestratorHandler as regular transactions, via processBySettlement(). No separate Lambda for accounting.

Vecnet — Build Spec v0.2 · Obsidian Terminal