Payments, ledger, and payouts
From 1.4 you saw at-most-once delivery chosen because pings are cheap. Money is the opposite pole: a duplicated capture is a charged-twice rider and a support fire. Financial state therefore pays the full price for exactly-once effects.
A first pass might retry a timed-out capture as a new request and repair duplicate charges later from provider reports. That works only while the failure case is absent. One trip operation has one stable financial identity and produces at most one provider effect and one balanced, append-only ledger effect.
The flow follows the trip: authorize (place a hold) when the ride is requested, so we learn about bad cards before a driver drives; capture the final fare at completion; refund/adjust via support flows. Tips capture separately. Drivers accrue earnings per trip and receive weekly payouts (batch, off-peak) minus the platform take. Promo credits and wallet balances are spent before the card is touched.
Two non-negotiables. First, every mutation to the payment service carries an idempotency key — trip id + operation — stored with the outcome, so retries (and there will be retries: PSP timeouts are routine) return the recorded outcome instead of re-executing. Second, every movement writes a double-entry ledger: equal and opposite entries across accounts (rider receivable, platform revenue, driver payable, PSP clearing). The ledger is append-only and is the substrate for earnings statements, refunds, financial reporting (1.13), and fraud analytics (2.11). The PSP knows about cards; our ledger knows about truth.
sequenceDiagram
autonumber
participant T as Trip service
participant PAY as Payments service
participant L as Ledger
participant PSP as Payment provider
Note over T,PSP: at ride request
T->>PAY: authorize {trip id, est. fare} — idempotency key trip:auth
PAY->>PSP: create hold
PSP-->>PAY: hold ok
PAY->>L: entry — rider receivable / pending
Note over T,PSP: at trip completion
T->>PAY: capture {trip id, final fare} — idempotency key trip:capture
PAY->>PAY: seen this key? no → proceed
PAY->>PSP: capture against hold
PSP--xPAY: timeout!
PAY->>PSP: retry same PSP idempotency key
PSP-->>PAY: captured (was already applied)
PAY->>L: entries — rider charged, driver payable 80%, platform revenue 20%
PAY-->>T: settled — receipt data
Note over PAY,PSP: weekly batch
PAY->>PSP: payout driver balances
PAY->>L: entries — driver payable cleared
flowchart LR
TRIP["Trip completed — fare 20.00"] --> PAY["Payments service"]
PAY --> LED
subgraph LED ["Double-entry ledger — append only"]
E1["Rider account −20.00"]
E2["Driver payable +16.00"]
E3["Platform revenue +4.00"]
end
LED --> STMT["Driver earnings statements"]
LED --> FIN["Financial reporting 1.13"]
LED --> FRAUD["Fraud analytics 2.11"]
LED --> REC["Daily reconciliation vs PSP settlement file"]
Never make trip completion wait on capture. If the PSP is down, the trip still completes, the rider still leaves the car, and capture retries in the background — riders on hooks for PSP outages is self-inflicted downtime. This is your first taste of graceful degradation, formalized in 2.6.