When the last unit goes between one shopper's cart and another's checkout, exactly one order exists and the shopper who missed it keeps their cart, their code and their gift card balance.
Why now. The direction is selling one real thing, and the contended path is the one nobody has proved. orders.place() takes stock, redeems the discount and spends the gift card inside one transaction and rolls the lot back by throwing OUT_OF_STOCK: or CODE_GONE:; shop.placeOrder re-renders the checkout with that sentence. Not one test covers any of it — src/server.test.ts only proves that a variant seeded at zero shows "sold out" on its product page. A rollback nobody exercises is a rollback nobody knows about.
Done looks like
[ ] A unit test with stock at 1: two place() calls, the first ok, the second { ok: false } with "sold out while you were checking out"; exactly one row in "order", one stock_entry for the sale, and the variant's available stock is 0 — never below it.
[ ] The loser's rollback is asserted row by row: the discount's use count and its discount_redemption rows unchanged, the gift card's balance and gift_card_entry rows unchanged, no order_item, no order_event.
[ ] The same over HTTP: the losing shopper gets the checkout page back with the words, their cart cookie still holds the line and their code cookie still holds the code, and the page links back to /cart so the line can be changed rather than leaving them on a form that will fail again.
[ ] The emptied-gift-card case too: "That gift card no longer has that much on it.", balance untouched, cart intact.
[ ] A cart line that already exceeds what is on the shelf is shown as a warning on /checkout before the button is pressed, in the same words the cart uses (only N in stock).
[ ] Whatever the tests expose is fixed in the code, not papered over in the test; if the order number allocated to a rolled-back attempt is consumed, say so in a comment where the number is allocated.
[ ] The README says, in the paragraph about stock, what happens when the last one goes while you are typing your address.
Files I expect to change:src/units.test.ts, src/server.test.ts, src/pages/checkout.ts, src/orders.ts (only for what the tests expose), README.md.
Risks. This is one process: two place() calls are sequential, not threads — name the test for what it proves (the conditional UPDATE in takeStock is what makes the second fail) rather than pretending at a race. Do not weaken that conditional update to make a test easier. Start the file with freshData and depend on no other file's rows.
Whose brief fits:ledger — every state change a row, and the rollback proved.
When the last unit goes between one shopper's cart and another's checkout, exactly one order exists and the shopper who missed it keeps their cart, their code and their gift card balance.
Why now. The direction is selling one real thing, and the contended path is the one nobody has proved.
orders.place()takes stock, redeems the discount and spends the gift card inside one transaction and rolls the lot back by throwingOUT_OF_STOCK:orCODE_GONE:;shop.placeOrderre-renders the checkout with that sentence. Not one test covers any of it —src/server.test.tsonly proves that a variant seeded at zero shows "sold out" on its product page. A rollback nobody exercises is a rollback nobody knows about.Done looks like
place()calls, the firstok, the second{ ok: false }with "sold out while you were checking out"; exactly one row in"order", onestock_entryfor the sale, and the variant's available stock is0— never below it.discount_redemptionrows unchanged, the gift card's balance andgift_card_entryrows unchanged, noorder_item, noorder_event./cartso the line can be changed rather than leaving them on a form that will fail again./checkoutbefore the button is pressed, in the same words the cart uses (only N in stock).Files I expect to change:
src/units.test.ts,src/server.test.ts,src/pages/checkout.ts,src/orders.ts(only for what the tests expose),README.md.Risks. This is one process: two
place()calls are sequential, not threads — name the test for what it proves (the conditionalUPDATEintakeStockis what makes the second fail) rather than pretending at a race. Do not weaken that conditional update to make a test easier. Start the file withfreshDataand depend on no other file's rows.Whose brief fits: ledger — every state change a row, and the rollback proved.