Skip to content

Transaction Processing Flow

Real-time transaction processing: EventBridge events received via SQS, routed through fees and accounting.


Overview

AttributeValue
TriggerSQS (TransactionEventsQueue FIFO)
SourceEventBridge bus usrv-data-sync-sls-{stage}-transactions-bus
Handlersrc/handler/transactionOrchestratorHandler.ts
Servicesrc/service/OrchestratorService.ts
FiltersPAYMENTS, APMS, WITHDRAWALS, DISPUTES

The orchestrator normalizes incoming events, validates idempotency, checks module flags, calculates fees, and generates accounting journals. It handles both transaction events (PAYMENT, WITHDRAWAL, etc.) and settlement/reserve events (SETTLEMENT_APPROVE, ROUTING, etc.) that arrive on the same queue.


Flow Diagram


Detail-Type Routing Table

Detail-TypeRouteNotes
PAYMENTS.PAYMENTprocessByTransactionCard payment
APMS.PAYMENTprocessByTransactionAlternative payment method
WITHDRAWALS.WITHDRAWALprocessByTransactionPayout to merchant
WITHDRAWALS.TOPUPprocessByTransactionMerchant top-up
PAYMENTS.REFUNDprocessByTransactionRefund
PAYMENTS.DISPUTEprocessByTransactionDispute / chargeback
PAYMENTS.VOIDprocessByTransactionVoid
SETTLEMENTS.SETTLEMENT_APPROVEprocessBySettlementSettlement approval
SETTLEMENTS.SETTLEMENT_CONFIRMprocessBySettlementSettlement confirmation
SETTLEMENTS.ROUTINGprocessBySettlementRouting fee
SETTLEMENTS.ROLLING_RESERVE_RELEASEprocessBySettlementReserve release
PAYOUT (legacy)processByTransactionNormalized → WITHDRAWAL
WAA_DEPOSIT (legacy)processByTransactionNormalized → TOPUP

Transaction Data Extraction

The raw SQS record embeds an EventBridge event with a DynamoDB NewImage payload. getTransactionData() extracts and normalizes into two typed structures:

IPayInData (PAYMENT, APMS_PAYMENT, REFUND, DISPUTE, VOID)

FieldSource pathNotes
businessIddetail.keys.business_id
transactionIddetail.keys.id
amountdetail.data.NewImage.amount
acqdetail.data.NewImage.acquirerlowercased
providerdetail.data.NewImage.providerlowercased
currencyCodedetail.data.NewImage.currency_codemay be missing → enriched
currencyIddetail.data.NewImage.currency_idused if currencyCode missing
paymentMethodIddetail.data.NewImage.payment_method_id
issuingCountryIddetail.data.NewImage.issuing_country_idoptional
cardBranddetail.data.NewImage.card_brandoptional
paymentIddetail.data.NewImage.payment_idoptional, metadata
checkoutIddetail.data.NewImage.checkout_idoptional, metadata
trxCreatedAtoperation_date | created | created_atepoch ms
categorydetermineJournalCategory(type, status)

IWithdrawalData (WITHDRAWAL, TOPUP)

FieldSource pathNotes
businessIddetail.keys.business_id
transactionIdTOPUP: ${businessId}#${acquirer_reference}; else detail.keys.id
amountdetail.data.NewImage.amount
acqdetail.data.NewImage.acquirerlowercased
methoddetail.data.NewImage.methodtransfer method
trxCreatedAtoperation_date | created | created_atepoch ms
categorydetermineJournalCategory(type, status)

Validation

validateIncomeData

Event typeRequired fields
PayInpaymentMethodId, currencyId, acq, businessId
Withdrawalacq, businessId

Throws AppError(E004) if validation fails → result: MISSING_INFO.

validateTrxAlreadyProcessed

DynamoDB query:

  • Table: JournalsTable
  • Index: process_id-index
  • Key: process_id = transactionId
  • Filter: category = category

Returns true if any journal exists (idempotency guard).


Data Enrichment

enrichData() fills missing currencyCode before fee calculation:

ConditionAction
currencyCode presentSkip (no MongoDB call)
currencyCode missing, currencyId presentfindOne in currencies collection by id
Both missingaggregate on business collection with $lookup to currencies

Result Codes

CodeMeaning
PROCESSEDTransaction fully processed (fees + accounting)
ALREADY_PROCESSEDDuplicate: journal already exists in DynamoDB
MISSING_INFORequired data missing or fee rule not found
MODULE_DISABLEDFEES_CALCULATION module disabled for this entity
NOT_SUPPORTED_DETAIL_TYPEUnknown event type

Middleware Stack

warmupMiddleware()
  └─ inputOutputLoggerMiddleware()
       └─ BUILDER_SQS_MIDDLEWARE()           ← parse SQS records
            └─ makeHandlerIdempotent()        ← DynamoDB IdempotencyTable, TTL 120s
                 └─ MONGODB_CONNECTION_MIDDLEWARE()
                      └─ OrchestratorService.transactionOrchestrator()

Idempotency key: conditional_idempotency_key(@) — custom JMESPath function that extracts the transaction ID from the SQS record. If parsing fails, returns null (idempotency skipped).


Account Updater Handler

src/handler/accountUpdaterHandler.ts — consumes AccountUpdateQueue (FIFO) and calls AccountService.accountUpdater().

AttributeValue
QueueAccountUpdateQueue FIFO, maxReceiveCount=160
Handlersrc/handler/accountUpdaterHandler.ts
Servicesrc/service/AccountService.tsaccountUpdater()
MiddlewarewarmupMiddleware, inputOutputLoggerMiddleware, BUILDER_SQS_MIDDLEWARE

Messages arrive after accounting journals are persisted (see accounting.md). Each message contains ledger entries for a single account. accountUpdater() applies entries sequentially, maintaining running balance and annotating each entry with balance_before/balance_after.


Non-Obvious Behaviors

BehaviorDetail
2-day cutoffnow - trxCreatedAt > 172_800_000ms returns ALREADY_PROCESSED. Only applies in real-time flow — bulk reprocess bypasses this.
TOPUP transaction IDConstructed as ${businessId}#${acquirer_reference}, not the raw record ID.
PAYOUT / WAA_DEPOSITLegacy detail-types silently normalized before routing. No error, no log warning.
FIFO per businessIdMessageGroupId = businessId ensures ordering within a business; different businesses process in parallel.
Idempotency TTL 120sIdempotencyTable caches responses for 2 minutes. Duplicate SQS deliveries within that window get the cached result without reprocessing.
Module flagsFEES_CALCULATION can be disabled per entity via FinancesConfigService. If disabled, returns MODULE_DISABLED without calling fees or accounting.
Fixed APM acquirersbitso, oxxopay, mercadopago always map to DetailTypeEnum.APMS_PAYMENT regardless of transaction_type.
processBySettlement skips feesSettlement/routing/reserve events go directly to accountingPreparer() — no fee calculation step.
E003/E004 → MISSING_INFOKnown AppError codes (missing account, missing data) return MISSING_INFO and record a CloudWatch metric. Other errors propagate and fail the SQS message.

Vecnet — Build Spec v0.2 · Obsidian Terminal