Skip to content

Transaction processing (orchestrator)

Each finances flow keeps Tonder's semantics and replaces the plumbing.

Trigger: the Listener receives a rail notification (Tonder Backend or Menta Backend), normalizes it into a transaction_events row (or pgmq message) with a correlation_id. A worker consumes it.

Per-event steps (inside one transaction)

  1. Dedup / order: advisory lock on entity_id; if a journal already exists for (process_id, category), return ALREADY_PROCESSED.
  2. Normalize: map the rail payload → internal payin or withdrawal shape. (Tonder's PAYOUT→WITHDRAWAL, WAA_DEPOSIT→TOPUP normalizations: keep the concept, no EventBridge naming.)
  3. Validate required fields → MISSING_INFO if absent.
  4. Cutoff: event older than 48h → ALREADY_PROCESSED (realtime only). See the 2-day cutoff.
  5. Module check: FEES_CALCULATION enabled for this entity+acquirer? If not → MODULE_DISABLED.
  6. Enrich: fill currency_code if missing (a SQL lookup now, not a Mongo call).
  7. FeesAccounting.

Result codes carry over: PROCESSED, ALREADY_PROCESSED, MISSING_INFO, MODULE_DISABLED, NOT_SUPPORTED_DETAIL_TYPE.

Settlement / routing / reserve-release events skip fees and go straight to accounting (Tonder's processBySettlement).

Persist every attempt

Before (or alongside) the steps above, the orchestrator upserts a transactions row for every event regardless of outcomesuccess, declined, failed, pending, expired. Journals are created only for the successful financial subset; the attempt row is what powers acceptance/conversion/risk metrics. A declined or expired notification produces a transactions row and no journal.

Vecnet — Build Spec v0.2 · Obsidian Terminal