DEV Community

Taocarts跨境
Taocarts跨境

Posted on AI-assisted

How Taocarts Keeps Reverse-Taobao Orders From Double-Charging Customers

How Taocarts Keeps Reverse-Taobao Orders From Double-Charging Customers

Taocarts, a reverse-taobao shopping and consolidation service, lets overseas buyers purchase from Taobao, 1688 and a few other Chinese marketplaces, then ships everything as one consolidated parcel. Every order can touch three wallets: the buyer pays us, we pay the source marketplace, and a logistics provider charges us for the international leg. The single ugliest bug in this kind of system is a double charge — and it is far easier to introduce than you would think.

Where double charges sneak in

The naive implementation fires the purchase call and the shipping call as two separate steps. If the shipping step times out and the job gets retried, the retry may re-run the entire flow, buying the item a second time. We learned this the hard way during a domestic sales event when our queue worker restarted mid-flight and replayed a batch of jobs.

The root cause was that "has this order been purchased" was decided by checking side effects (does a purchase record exist?) instead of by an explicit, atomic state transition.

Modeling the order as a state machine

We made the order a strict state machine on the Laravel side: CREATED → PAID → PURCHASED → SHIPPED → RECEIVED. Each transition is guarded and idempotent. Moving from PAID to PURCHASED is wrapped in a database transaction with a row lock:

DB::transaction(function () use ($order) {
    $order = Order::where('id', $order->id)->lockForUpdate()->first();
    if ($order->status !== Order::PAID) {
        return; // already handled, skip silently
    }
    $this->taobaoClient->createPurchase($order);
    $order->status = Order::PURCHASED;
    $order->save();
});
Enter fullscreen mode Exit fullscreen mode

Because the transition checks the current status inside a locked row, a replayed job sees PURCHASED and returns early instead of buying twice.

Idempotency keys at the boundary

The state machine protects our database, but the source marketplace API is still external. Every purchase request carries an idempotency key built from the order ID plus the source trade_id. If the marketplace returns a duplicate-trade error, we treat it as success and reconcile rather than retrying blindly.

Why this matters for consolidation

In a consolidation service, one buyer cart can span several source orders, each with its own purchase and shipping step. Without a hard state machine and idempotency keys, a single worker restart during a traffic spike can quietly multiply charges across dozens of carts. Since we wired this in, the nightly reconciliation job — which sums settled amounts against sourced costs — has shown zero unexplained gaps.

One more practical guard: we cap the number of in-flight purchases per order and surface a "pending" state to the buyer the moment payment clears, so they never stare at a blank screen wondering whether they were charged twice. That UI honesty buys more trust than any retry logic we could write.

The lesson is boring but real: don't infer "done" from side effects. Put an explicit, locked state transition in front of every money-moving step, and your finance team will stop finding ghosts in the ledger.

Top comments (0)