Docs/Get startedv0.0.1-preview

Modelling common flows

Authorization and capture, wallet top-ups, marketplace splits, and treasury funding — mapped onto the ledger API.

On this page
  1. Card-style authorization and capture
  2. Wallet top-up and spend
  3. Marketplace split with escrow
  4. Treasury and settlement tiers
  5. Declines your ops team can see
  6. One habit that makes all of this work

Most value-movement products reduce to a handful of ledger shapes. This page maps the common ones onto TulaDB operations, so you can see your own system in the API before writing integration code.

Card-style authorization and capture

The authorization is a reserve; the capture is its commit; the void is its rollback. The customer’s available balance reflects the hold the moment it’s placed; posted balance moves only at capture.

authorize   → reserve(txn_id="auth-7781", amount=5000)      # PENDING, available -5000
capture     → commit_by_key(..., "auth-7781")               # POSTED
   — or —
void        → rollback_by_key(..., "auth-7781")             # VOIDED, funds released

Partial capture is not a single engine operation yet — commit settles the full reserved amount. Capture the full hold, then refund the difference as its own transfer (its own txn_id, e.g. auth-7781-refund), keeping every movement individually attributable and leaving no unprotected window; see partial capture for the alternative and its trade-off. The settlement worker retries safely forever: commit is idempotent, and a hold never silently leaks if the worker dies between steps.

Wallet top-up and spend

Money enters the ledger through a control account when the PSP confirms the top-up; spending is ordinary transfers between user wallets and merchant accounts.

top-up   → transfer(control → wallet, txn_id=psp_payment_id)
spend    → transfer(wallet → merchant, txn_id=order_id)
withdraw → reserve(wallet → control, txn_id=payout_id)      # hold during payout processing
           then commit (payout sent) or rollback (payout failed)

Use the PSP’s payment id as txn_id and webhook redelivery becomes harmless — the duplicate replays instead of double-crediting. The wallet UI reads available_balance (holds included); finance reads posted_balance; both are the same account, no shadow bookkeeping.

Marketplace split with escrow

A checkout that must end up 85/12/3 across seller, platform, and processor is a reserve at purchase, then a batch of transfers at release:

checkout  → reserve(buyer → escrow, txn_id=order_id)         # buyer funds held
delivery  → commit_by_key(..., order_id)                     # escrow funded
release   → transfer_batch([
               escrow → seller    (txn_id=order_id+"-seller"),
               escrow → platform  (txn_id=order_id+"-fee"),
               escrow → processor (txn_id=order_id+"-proc"),
            ])
refund    → rollback_by_key(..., order_id)                   # before release

Each leg has a deterministic txn_id derived from the order, so a crashed release job reruns its batch and the landed legs answer OK_DUPLICATE. A declined refund (already released) comes back ERR_WRONG_PHASE — a durable, supportable answer rather than a race. Retrying it is safe but gets the same answer; get_transaction_by_key on the order shows the hold POSTED.

Treasury and settlement tiers

When you track both positions (who owes whom) and settlement (what actually moved at the bank), use a position–settlement ledger and tiered transfers: one transaction moves both tiers atomically, so positions can never drift from settlement — the reconciliation between tiers is zero by construction, not by a nightly job.

settle → tiered_transfer(dr_position, cr_position,
                         dr_settlement, cr_settlement,
                         amount, txn_id=settlement_ref)

Declines your ops team can see

Whatever the flow, route business declines through the ledger rather than swallowing them: let limit checks decline naturally (ERR_LIMIT is recorded), and record upstream refusals — a fraud block, a compliance hold — with an explicit reject. Six months later, “show every declined payout for this account” is a query, not an archaeology project.

One habit that makes all of this work

Every pattern above leans on the same discipline: derive txn_id deterministically from the business event — the order id, the payment id, the settlement reference — never from a random UUID minted at send time. Deterministic identity is what turns crashes, retries, and redeliveries from correctness problems into noise. Read Idempotency once and this habit becomes obvious.