Originally published on Medium - cross-posting here since I know a lot of you spend more time on dev.to. This is the fifth post in a series on modeling orders and inventory for ecommerce backends.
A reader replied to the post on returns and partial fulfillment with a question about shipments that split and do not complete. Answering it properly turned up something worth being honest about upfront. There is not one failure mode hiding behind "the shipment did not work out". There are at least four, and they do not share a fix. This post walks through one of them in full. The other three, I will name - because you will hit them eventually - but not solve here.
Quick context if you are new here: a line item can ship across more than one shipment, and shipments need their own status, separate from the line item. That much is standard by now. What is not standard is what happens when the status of a shipment itself becomes a matter of dispute, not a fact.
Delivered, According to Whom?
A carrier scans a package as delivered. The customer says it never arrived. Both are true statements - about different things. The carrier is reporting a scan event. The customer is reporting their own experience. Nothing in a naive schema has a way to hold both of those at once, because a naive schema treats delivered as a fact, not a claim.
That distinction matters more than it sounds like it should. Trust the status from the carrier blindly, and "delivered" in your system quietly comes to mean "the scanner from the carrier said so". That is a weaker claim than what everyone reading that status assumes it means. Trust the customer blindly instead, and you are refunding orders that genuinely arrived, every time someone claims otherwise.
The fix is not a new shipment status. A dispute is not a state the shipment moves through - it is a claim layered on top of whatever the shipment status already says.
CREATE TABLE delivery_disputes (
id UUID PRIMARY KEY,
shipment_id UUID NOT NULL REFERENCES shipments(id),
status TEXT NOT NULL, -- 'open', 'resolved_delivered', 'resolved_not_delivered'
opened_at TIMESTAMPTZ NOT NULL DEFAULT now(),
resolved_at TIMESTAMPTZ
);

While a dispute is open, the shipment status stays exactly what the carrier reported. Nothing about the underlying stock changed - the ledger entry for that sale was correct the moment it was made, based on the information available at the time. This is a different kind of problem than a warehouse simply not having the stock it claimed to have. Here, the ledger was right. What is uncertain is a fact about the physical world, not about the data.
Resolution depends on what actually happened, which your system usually cannot determine on its own - GPS data, photos, or a carrier investigation resolve it, not a query. If the dispute resolves as resolved_delivered, nothing further happens; the original transaction stands. If it resolves as resolved_not_delivered, that is the moment for a real correction:
INSERT INTO stock_ledger (sku_id, delta, reason, reference_id, reference_type, note)
VALUES ($1, 0, 'delivery_dispute_confirmed', $2, 'manual_adjustment',
'carrier investigation confirmed package not delivered, reshipped from stock');
The delta is zero, on purpose. It is the same pattern as a non-restocking return elsewhere in this series. Nothing about available inventory changed, but the event is worth recording. "Why did we reship this order" is a question someone will eventually ask, and a silent edit to a payment record is not an answer.
Three More Ways a Shipment Quietly Breaks Your Numbers
Here is the honest part. Delivery disputes are not even the most common way this goes wrong - they are just the one most people recognize immediately, because everyone has been on one side of that argument as a customer. The other three are quieter, and each one needs a genuinely different fix, not a variation on this one.
One of them is about a package that goes missing after it genuinely left the warehouse - real stock, gone somewhere between your dock and the door of the customer. The ledger entry for that sale was correct. The fix has nothing to do with the ledger at all.
One of them is about a warehouse floor that cannot find stock your system swears is there - which means the number was wrong before this order ever came in, not after. That is a different class of problem than a lost package, and treating it the same way will leave your ledger quietly wrong in a way nobody catches for months.
One of them is what happens when the floor has some of what was promised, but not all of it - not a clean yes or no, the situation real warehouses hand you more often than either extreme.
Each of these needs its own schema decision, its own ledger entry, and - for at least one of them - a genuine "watch this fail, then watch it get fixed" moment that no amount of reading substitutes for. That is not a line for this article. It is Lesson 4 of the course I am building on this topic, the same one this whole series has been building toward: the schema, the live build, and - for the trickiest of the three - a demo where the wrong version breaks on screen before the correct one holds.
If you have read this far because you are staring at one of these three right now, the waitlist is the fastest way to find out when the rest is ready: join the waitlist - free, in the works now, you will be the first to know.
Where This Leaves You
A delivery dispute is a claim layered on a shipment, not a new status the shipment moves through - the ledger only changes once the dispute actually resolves as undelivered, and even then, the entry exists to record what happened, not because the original transaction was wrong. That is the one failure mode this post solves completely.
The other three are real, they are common, and they do not share this fix. If today is not the day you need them, this post still gave you the one that shows up the most: the argument between what a carrier scanned and what a customer experienced, and why your schema needs room for both to be true statements about different things.
This connects to Lesson 4 in the course I am building on this topic. If you want to know when it is ready: join the waitlist - free, in the works now, you will be the first to know.
A note on scope: like the earlier posts, this assumes delivery confirmation and dispute resolution flow through your own system in a reasonably timely way. A carrier or fulfillment partner that reports delivery status with long delays, or not at all, turns this clean two-state resolution into an ongoing reconciliation problem - a different, harder topic than what is covered here.
Top comments (0)