Skip to article

Money has to inherit the shape of the trip

Checkout is the front door, not the building. One button creates collections, supplier settlements, FX snapshots, refunds and ledger entries, and they only stay coherent if every movement carries the order's own identifiers.

The most expensive field in a travel product is paid: true. It looks like a fact. It is four different questions collapsed into one boolean, and it is only ever answering one of them.

Did the customer's money arrive? Is the travel service confirmed? Does the supplier have their money? Has finance recorded the economic event? Those four go out of sync constantly and legitimately. The card captures and the hotel fails. The hotel confirms and the bedbank will not be paid for thirty days. The supplier approves a refund and the traveller's bank shows nothing for a week. Every one of those is a correct state of the world, and none of them is paid.

Checkout is the front door, not the building. What sits behind it has to connect what the customer paid, what each supplier is owed, what the business earns and what changes in month four when the traveller moves the activity to another day. That lineage belongs on the item hierarchy described in the travel order lifecycle. A finance-only booking model living beside the real one is just a second source of truth, and you will spend every month reconciling the two long before you reach a bank statement.

Four questions, four state machines

Split the boolean into the questions it was hiding:

  1. Collection: did we successfully collect or authorise customer funds?
  2. Order: which travel components are confirmed, pending or failed?
  3. Settlement: which supplier obligation is due, through which rail and currency?
  4. Accounting: how do all movements map to revenue, cost, liability and refund positions?

These influence each other. They are not interchangeable. A successful card charge does not prove a supplier confirmed. A confirmed booking does not prove the supplier has been paid. A supplier-approved refund does not mean the customer sees the money yet.

Every movement carries the order's identifiers

Identifier lineage is the design decision that everything else rests on. Every financial event should trace back through one hierarchy:

trip → order → component → supplier obligation → payment movement

Customer collections, virtual-card authorisations, bank transfers, fees, FX adjustments, refunds and chargebacks all carry enough reference data to rejoin that chain.

Do not key on the supplier's reference or a free-text descriptor. Supplier references arrive late, change format between releases and sometimes cover several components at once. Your own immutable identifiers anchor the system; external references are indexed attributes, useful for lookup, never load-bearing.

OnArrival's Payments product is organised around a travel ledger that follows money from offer through booking, segment and ancillary. Build your own if you prefer. The principle does not change: money inherits the structure of the trip, or finance reverse-engineers it every month.

A practical state model

Use explicit state machines. One per question.

ObjectExample statesCritical question
Customer paymentcreated, authorized, captured, failed, reversed, refundedWhat did the customer’s payment method do?
Booking componentpending, confirmed, changed, cancelled, failedWhat travel service currently exists?
Supplier payablenot due, scheduled, issued, accepted, failed, nettedWhat do we owe the supplier?
Refundrequested, supplier-approved, funded, submitted, completed, failedWhere is the return of funds now?
Ledger entrypending, posted, reversedHas finance recorded the economic event?

Transitions are event-driven, timestamped and idempotent. Store why a transition happened and which command or external message caused it. A state machine that cannot explain its own last hop is a status field with extra steps.

One charge, a settlement tree

Take a bundle: flight, hotel, activity. The customer sees one charge. The supplier side needs an airline settlement path, a hotel payment on a different rail and a third schedule for the activity operator. Margin belongs partly to each component. A cancellation touches one branch.

Collect it as one authorisation for the total, then capture per component as each supplier confirms. That is the single-auth, staged-capture pattern the travel cart essay derives from the commit saga. Here is why it matters for money rather than for inventory: each capture is born knowing which component it pays for. Capture the total in one movement and you throw that away at the exact joint where customer money meets supplier money. Nobody recovers it later. They approximate it, in a spreadsheet, in month four.

COLLECTION COMPONENT CAPTURES SUPPLIER SETTLEMENT captured per confirmation, never before CUSTOMER one auth · INR 52,300 C1 · FLIGHT capture INR 31,400 C2 · HOTEL capture INR 17,600 C3 · ACTIVITY capture INR 3,300 AIRLINE · EUR 316.00 virtual card BEDBANK · USD 168.75 bank transfer · due T+30 OPERATOR · THB 1,190 rail per supplier hotel cancels later REFUND WALKS THE SAME EDGES BACK supplier recovery · reverses capture C2 only · INR 17,600 to the original method
One authorisation fans out into component captures, each capture funds one supplier obligation, and a refund walks the same edges backwards.

The settlement plan is therefore generated from confirmed order components, never from the cart total. Each payable needs:

  • Supplier and booking-component identity
  • Amount and settlement currency
  • Due date or release condition
  • Selected rail
  • FX rate and source where relevant
  • Current position, retries and external reference
  • Links to adjustments or refund netting

Virtual cards, industry settlement systems and bank rails all fit this shape without anyone pretending they behave alike. They do not. The unifying layer is the ledger, not the rail; teams that try to unify at the rail end up with a generic payout abstraction that is wrong about every rail equally.

FX is order data, not a display concern

When display, collection and supplier settlement currencies differ, the exchange rate moves both customer experience and margin. Record the rate used at quote, any lock or expiry, the rate applied at capture or settlement, and the resulting variance.

Never recompute a historical amount at today's rate. A reporting query that multiplies stored foreign amounts by a live rate looks correct in staging and returns a different answer every morning in production. The order needs an immutable monetary snapshot: currency, precision, price components, rate context and timestamps.

When a booking changes, write adjustment entries. Do not rewrite the past. Finance has to be able to state the difference between what was promised and what happened.

Refunds reverse economics, not history

A refund references the original payment and the specific cancelled component. It does not delete the sale, and it does not mutate a capture into a smaller fictional one. Mutating the capture is the tempting shortcut because the totals agree instantly. It also destroys the only record of what the customer agreed to pay.

A refund flow has to answer:

  • What is the customer entitled to under the accepted rules?
  • Has the supplier approved or returned the corresponding value?
  • Is the business advancing funds before supplier recovery?
  • Which fees or penalties remain?
  • Which payment method receives the refund?
  • What ledger entries reverse revenue, cost or liability?
  • Can part of the supplier position be netted against a future payable?

A refund is a first-class object that points backwards. For the cancelled hotel in the diagram above:

{
  "refund_id": "ref_204",
  "order_id": "ord_7ac1",
  "component_id": "cmp_hotel_2",
  "original_capture": "cap_c2",
  "customer_amount": { "currency": "INR", "value": 17600 },
  "supplier_recovery": {
    "payable_id": "spl_02",
    "expected": { "currency": "USD", "value": 168.75 },
    "state": "supplier_approved",
    "settlement": "net_against_next_payable"
  },
  "fx_context": "rate snapshot from cap_c2, never today's rate",
  "advanced_before_recovery": true,
  "ledger_entries": ["reverse_revenue", "reverse_supplier_cost", "recognize_recovery_receivable"]
}

advanced_before_recovery is the field finance argues about, and they are right to. Refunding the traveller before the bedbank returns the USD is usually the correct product decision: nobody should wait thirty days on a supplier's settlement calendar. But that float is a real position. It belongs on the books as a receivable, not netted quietly into a number that looks fine only while volume is small.

Reconciliation is a daily job or it is a fiction

Month-end should confirm what the system already knew. If month-end is where you discover things, you do not have reconciliation, you have an audit.

Reconcile at several levels:

  1. Gateway or bank movement to payment object
  2. Payment object to customer order
  3. Supplier payout to payable and booking component
  4. Refund movement to refund object and original collection
  5. Ledger balances to external statements and safeguarded or operating accounts as applicable

Type the exception queues by cause: missing reference, amount mismatch, currency mismatch, unexpected duplicate, overdue supplier acceptance. One generic unmatched bucket converts a specific product defect into a growing pile that nobody owns and everyone learns to scroll past.

The bundle from the settlement tree above crosses at least four external statements over its life, and each line has exactly one internal record it must match.

External statement lineAmountShould matchOutcome
Gateway payout, batch PG-8841+INR 52,300Payment pay_9f2: captures C1, C2, C3Matched at capture level
Virtual card clearing, airline-EUR 316.00Payable spl_01 for component C1Matched; INR 12.40 funding-rate variance posted as an adjustment
Bank debit, bedbank-USD 171.00Payable spl_02, scheduled at USD 168.75Exception: amount mismatch, typed supplier-overcharge, owner assigned
Card refund, ref RF-204-INR 17,600Refund ref_204 against capture C2Matched to the original collection; USD 168.75 recovery receivable stays open

Rows one, two and four confirm the system. Row three is the point. The mismatch is caught the day the statement arrives, typed, owned and sized at USD 2.25, instead of joining an unexplained write-off eight months later when nobody can name the booking it came from.

One vocabulary, three audiences

The itinerary says "cancelled", finance says "refund submitted", the bank says "processing". All three are true at once. They are not contradictions to resolve; they are three views of one timeline, and the product's job is to connect and translate them.

Support gets read access to the financial timeline and never to raw payment credentials. Finance gets component and supplier context without filing a support ticket. The customer gets the part that affects them: amount, destination, current stage, next expected event. The post-booking trust guide covers how those operational states become customer language.

The same model earns its keep during incidents. When a payout rail fails, operators can name the affected obligations instead of treating every booking as suspect. When capture succeeds and confirmation does not, recovery or reversal becomes a decision someone makes, not a race between two retry loops.

Before you launch

Ask:

  • Can every cent be traced to an order and component?
  • Are collection, booking, settlement and refund states separate?
  • Are commands idempotent across retries and webhooks?
  • Are price and FX snapshots immutable?
  • Can partial changes create adjustments without rewriting history?
  • Does supplier settlement start from confirmed obligations?
  • Can finance explain a customer refund from a single timeline?
  • Are reconciliation exceptions typed and owned?
  • Can support see financial status without seeing sensitive credentials?
  • Do customer-facing labels reflect what the system actually knows?

Ten no answers is a rebuild waiting for a trigger. One customer action creates many movements. They only ever tell one story if they were born knowing which part of the trip they belong to.