Modeling a Refund Ticket Flow with Original-Currency Reconciliation
When you run a reverse-Taobao platform that collects payments in euros, dollars, won, and ringgit but settles suppliers in RMB, a refund stops being "just reverse the charge." The customer paid in their currency; your ledger lives in another. Get the conversion wrong and you either eat the loss or quietly short-change the buyer.
Taocarts, a cross-border system for agents reselling from Taobao and 1688, treats refunds as a first-class workflow rather than an afterthought. Here is the model we landed on after a few painful edge cases.
The problem with naive reversal
The tempting design is: on refund, look up the original payment, call the gateway's refund API for the same amount. That breaks the moment exchange rates move. A buyer who paid 100 EUR when EUR/CNY was 7.8 gets refunded 100 EUR when the rate is 7.6. They receive less RMB-equivalent than they paid, and they notice.
Worse, your finance team needs to reconcile against the RMB supplier cost, not the live rate. Two currencies, three numbers, zero traceability—exactly the mess audits hate.
Modeling a refund as a ticket
We stopped thinking in terms of "reverse the transaction" and started thinking in terms of a refund ticket with explicit state:
-
requested— buyer opens it with reason and evidence. -
reviewing— agent checks the goods, the shipping, the policy. -
approved— amount and currency locked. -
reconciling— original-currency value computed and frozen. -
refunded— gateway call made, proof attached. -
closed— ledger updated, supplier side settled if applicable.
Each transition writes an append-only event. No state is skipped silently.
Freezing the original currency
The key field is original_settlement_amount: the RMB-equivalent captured at purchase time, stored immutably. When a refund is approved, we compute the payout as min(buyer_paid_in_local, original_settlement_amount_converted_at_refund_rate) depending on policy, and we always record both numbers. The buyer sees their local amount; finance sees the RMB truth.
This matters because some disputes are partial—a broken item, not a missing one. The ticket carries a refund_ratio so a 30% damage claim maps to 30% of the frozen amount, not a guess.
Why a ticket, not a button
A button hides the work. A ticket makes the work visible: who approved, at what rate, with what evidence. When a buyer asks "where is my money," support answers from the ticket, not from memory.
We also added a hard rule: a refund cannot move to refunded until reconciling has both currency values written. If the rate service is down, the ticket waits in approved. Better a slow refund than a wrong one.
The payoff
Refunds dropped from "a phone call and a prayer" to a queryable pipeline. Disputes about "you sent me less than I paid" went to near zero once we started showing the original-currency breakdown on the refund confirmation.
Cross-border refunds are a currency problem wearing a payments costume. Model the currency explicitly, make every step a recorded event, and the scary part of running international sales—giving money back—becomes the most auditable part of your system.