
The problem
A storefront that takes real money has to assume the browser is hostile and the network is unreliable. A cart can sit open for a week, two shoppers can want the same last unit at the same moment, and every automated payment path can report success with nothing having actually moved.
Decisions
The rebuild started with what was already broken
Furniworld was rebuilt in place from an earlier, half-finished version of itself, so the first pass was repair rather than features: a const reassignment that broke production login, a User schema handed three argument objects so `timestamps` was silently dropped, operator injection in the query builder, pagination totals computed without the filter applied, and security middleware installed but never wired up. Only the first of those announced itself. The other four ran exactly as written and were wrong anyway, which is the kind that survives a rewrite if nobody goes looking.
The browser holds ids, the server holds prices
The cart in the browser carries only product ids and quantities; the server prices every line on read, and a guest cart is merged into the account on sign-in. A cart left open for a week cannot buy at last month’s price, because it was never carrying a price to begin with.
Order lines are snapshots, and products are archived, never deleted
Name, slug, image and unit price are copied onto the order and never re-read from the product for display, so an order keeps showing what was bought at the price paid after the listing is edited. Products are archived for the same reason, because deleting one would take the history of every order that contained it.
Stock is claimed by a conditional update
A findOneAndUpdate guarded by `stock: { $gte: quantity }` claims the units, and rolls back what it already claimed if a later line in the same order fails. Two shoppers racing for the last unit cannot both win. A read-check-write sequence allows exactly that, in the gap between the read and the write.
The order workflow is exported from the model
ORDER_TRANSITIONS lives on the order model, the controller validates against it, and the admin status dropdown builds its options from it. The alternative is a list of valid statuses maintained in two files, which stays correct exactly until the first time it doesn’t.
The hard part
What the gateway says and what actually happened are different fields
Card payments run real Stripe test-mode PaymentIntents and M-Pesa sends a real Daraja sandbox STK push, alongside cash on delivery and bank transfer. Not one of them is proof that the money arrived. A gateway callback, a delivery driver, or a customer typing a reference are all claims, not settlements.
So the order carries two fields: what the gateway reported, and what a person has verified. One method is the only place gateway outcomes are written and it never touches the verification field; an admin queue is where the second one gets resolved. Collapsing them into a single status is the version that quietly ships an order because a sandbox said OK.
The second half of the problem was vocabulary. Left as stored, the two fields contradict each other on screen: an order cancelled after it was paid for, or a cash sale reading as unresolved in one column and unresolved again in the next, for reasons that have nothing to do with each other. Customer-facing payment wording comes from its own module, and the stored value is never printed anywhere.