I put a $25 gift card in the cart and typed WELCOME10. The cart answered ?m=applied, drew "Remove WELCOME10" under the totals, and took nothing off: subtotal $25.00, total $25.00, no discount line and no sentence. The rule underneath is right — a gift card is money moving, not a sale, so goods_cents is 0 and ten percent of nothing is nothing — but the shopper is told they have a discount and then charged full price.
shop.applyCode asks discounts.evaluate(code, quote.goods_cents) and only refuses what the discount's own rules refuse (unknown, expired, under its minimum). A code that is valid and worth zero against this cart passes that check and is written into the codes cookie.
Done looks like
[ ] Applying a code that would take nothing off the cart as it stands is refused on the page it came from, in a sentence that says why — a percent or amount code against a cart of only gift cards, and a free-shipping code on a cart whose shipping is already free — and writes no cookie.
[ ] The sentence names the code and the reason in the shop's own words ("WELCOME10 takes 10% off goods, and a gift card is money moving, not a sale"), never a rule number.
[ ] A code that was worth something when it was applied and is worth nothing now — the goods came out of the cart and a gift card stayed — is not silently dropped: the totals block says the code is applied and taking nothing off, and the checkout page says the same thing in the same words.
[ ] A code that is worth something is unaffected: WELCOME10 on a cart of coffee still takes 10% off, and SHIPFREE over its minimum still frees the shipping.
[ ] Tests in src/server.test.ts or src/units.test.ts for each of the four: refused on a gift-card-only cart, refused when shipping is already free, still applied on ordinary goods, and the cart that changed under an applied code.
[ ] The refusal goes through codeRefused, which already renders on the page it came from — no new path.
Filessrc/shop.ts (applyCode), src/cart.ts if the quote has to say what a code was worth, src/pages/cart.ts and src/pages/checkout.ts for the totals block, the tests.
Risks "Worth nothing" has to be computed from the same quote the page draws, or the message and the total disagree; do not add a second pricing path. Keep the rate limit on applyCode in front of the new work. A cart that changes is the fiddly case — a shopper must never lose a code silently, so prefer saying it over deleting it.
Suggested ledger, with juniper reading the wording.
I put a $25 gift card in the cart and typed
WELCOME10. The cart answered?m=applied, drew "Remove WELCOME10" under the totals, and took nothing off: subtotal $25.00, total $25.00, no discount line and no sentence. The rule underneath is right — a gift card is money moving, not a sale, sogoods_centsis 0 and ten percent of nothing is nothing — but the shopper is told they have a discount and then charged full price.shop.applyCodeasksdiscounts.evaluate(code, quote.goods_cents)and only refuses what the discount's own rules refuse (unknown, expired, under its minimum). A code that is valid and worth zero against this cart passes that check and is written into the codes cookie.Done looks like
WELCOME10on a cart of coffee still takes 10% off, andSHIPFREEover its minimum still frees the shipping.src/server.test.tsorsrc/units.test.tsfor each of the four: refused on a gift-card-only cart, refused when shipping is already free, still applied on ordinary goods, and the cart that changed under an applied code.codeRefused, which already renders on the page it came from — no new path.Files
src/shop.ts(applyCode),src/cart.tsif the quote has to say what a code was worth,src/pages/cart.tsandsrc/pages/checkout.tsfor the totals block, the tests.Risks "Worth nothing" has to be computed from the same quote the page draws, or the message and the total disagree; do not add a second pricing path. Keep the rate limit on
applyCodein front of the new work. A cart that changes is the fiddly case — a shopper must never lose a code silently, so prefer saying it over deleting it.Suggested ledger, with juniper reading the wording.