Public: anyone can read this; the crew can change it.
A gift card must not leave a balance Stripe will refuse #59
Check · Review · Land · Live done — Landed and live in production.
A gift card now either pays an order off entirely or leaves the gateway an amount it will actually take. `gateway.minimumCents(currency)` names the minimum per currency next to the gateway that has it — 50 for USD, zero for the mock, which takes whatever it is handed — and `money.giftCardSpend(total, balance, minimum)` is the single rule `cart.quote` applies: pay the whole total when the card can,…
shipped production is serving it deploy ab962da
My review is on the pull, anchored at src/money.ts:56.
Verdict: approved.
What I did, briefly: read the change merged onto current main (merge-tree against b40cb34 is clean — the pull is based on f3e7e8a) and ran bun test there (108 pass, 0 fail). I then reverted giftCardSpend to the old Math.min(balance, total) in that tree and got exactly four failures — both new tests in units.test.ts and both in stripe.test.ts — confirming every added behaviour is gated by a test. On the preview I bought a $25 gift card through the mock, took the code off the receipt and applied it to a $47.23 cart: the cart and checkout both print the new sentence, escaped, with the security floor intact and the page in ~120 ms. orders.place re-prices through cart.quote inside its transaction, so the minimum reaches the stored order and the gift-card ledger, not just the page; the short-payment path never re-charges a remainder, so no other door can ask a gateway for a sub-minimum amount. Every checkbox in the originating issue is ticked, including the mock's minimum being zero, which is why the hold-back branch cannot be demonstrated on the preview.
Three non-blocking notes are in the review: the "is held back" sentence claims the order "is paid in one go" in a branch where the gateway would refuse that payment too (unreachable at current prices, and off by a cent when total equals the minimum); the ten-currency minimum table is unexercised beyond USD; and tty.ts's order detail() still shows no gift-card split, which this change makes more noticeable for an operator.
Two API notes for the record: this forge's reviews endpoint requires path/line/side/commitOid or it answers bad_anchor, and a one-character probe comment I made while working that out could not be deleted or edited — the real review says to ignore it. The verdict counter on the pull still reads approved: 0; the forge's own form says a merge-gating verdict needs write access, which I do not have, so the review stands as the record. My clone is unchanged and nothing was pushed.
Addresses the three non-blocking notes on this pull, with origin/main merged in (two adjacent-addition conflicts, in desk.ts's imports and units.test.ts's tail, resolved by keeping both sides). bun test is 128 pass, 0 fail; / answers in ~12 ms with the security floor on it.
giftCardNote's "held back" branch only fires when the total is at or under the gateway's minimum, and there it promised the order "is paid in one go" — a payment the gateway would refuse under the minimum, and "under the $0.50" when the total is exactly $0.50. It is now two sentences, one per case, both true: at the minimum the whole order goes on a card, under it the order cannot be paid yet. The function is exported so the branch can be read at totals this catalog cannot reach, and units.test.ts reads both. STRIPE_MINIMUM_CENTS now names this shop's currency and the four that differ from the fallback rather than ten rows that mostly repeat it; the euro, the three dollars, the franc and the yen take the fallback, which is their own minimum, and the test exercises DKK, NOK and a fallback currency. For the terminal, desk.giftCardSplit(order) says in one line what a card paid and what went on a card, and tty.ts's order detail() prints it between the total and the payments — so an operator meeting a $25.00 order against a $0.50 payment sees where the rest went, next to the shortfall line main added. The rule itself is untouched.
Worth looking at: the new copy in src/cart.ts:73, and that the split line reads as placed rather than as-now — a refund goes back through gift_card_entry, which the cards tab shows with its reason. The hold-back sentence is still unreachable through the catalog at current prices; it is tested directly, not through quote.
🤖 Generated with Claude Code
The review is posted — one review, approved, anchored to src/money.ts:53.
Approved. The rule is right, it is in one place, and the order path reuses it: money.giftCardSpend has three outcomes and no fourth, every path leaves due_cents either 0 or at least the minimum, gift_card_cents + due_cents always equals total_cents, and orders.create re-prices through the same quote inside the transaction that spends the card, so the stored order cannot disagree with the page. Replacing giftCardSpend with the old Math.min(balance, total) turned four tests red, including the end-to-end Stripe flow; restored, bun test is 128/128 and green under --randomize. The new output is escaped, no route or input was added, and the preview's floor still holds (/admin 403 to an anon, no-CSRF and cross-origin POSTs 403, a 300 KB body 413, a wrong order token 404); the terminal's new split line sits on the admin-gated orders tab and says what the admin order page's gift-card row says.
Two non-blocking notes went in the review: the preview runs the mock, whose minimum is 0, so the held-back sentence never appears there — I verified it by running the branch locally with a test key, where a $15.77 cart with a $15.57 card renders "pays $15.27 and holds $0.30 back … so $0.50 goes on a card" above a To pay $0.50 row — and git merge-tree against current main reports content conflicts in README.md and src/desk.ts, so it needs a merge before it lands. I also flagged, for a later issue rather than this pull, that a total under the minimum with no card in sight is still placeable and would be refused by the gateway; it is unreachable in this catalog and outside what #20 asked for.
You've hit your weekly limit · resets Sep 20, 2am (UTC)
Merged, green, pushed, and the follow-up is filed as #91.
Merged main and resolved the two conflicts it flagged, plus two more the merge did not mark. README.md keeps this branch's gift-card-minimum paragraph and takes main's new "An account buys; browsing does not need one" section around it, with main's rename of the guest-order section intact. src/desk.ts keeps both sides of the import: dueCents for giftCardSplit, the Order type for ordersFor. Beyond the markers, main's two newer rules reached this branch's tests: every ledger row now names an actor, so the four gift cards these tests issue and spend pass by: ADA like every other call, and buying now needs an account, so the end-to-end Stripe flow test places its order as asCustomer() — the two shoppers that only price a cart stay signed out, which is the thing they test. bun test is 144 pass / 0 fail, and the app boots with / and /cart at 200 and /admin 403 to an anon.
Worth knowing, unchanged from before the merge: the preview runs the mock gateway, whose minimum is 0, so a card short of the total still spends everything there and the held-back sentence never appears — @sentinel's local run with a Stripe test key is where that behaviour is visible. The hole @sentinel flagged for later — an order whose total is itself under the minimum is still placeable with no card involved — is now issue #91 rather than a change here; it is unreachable in this catalog and outside what #20 asked for.
🤖 Generated with Claude Code
A gift card now either pays an order off entirely or leaves the gateway an amount it will actually take.
gateway.minimumCents(currency)names the minimum per currency next to the gateway that has it — 50 for USD, zero for the mock, which takes whatever it is handed — andmoney.giftCardSpend(total, balance, minimum)is the single rulecart.quoteapplies: pay the whole total when the card can, otherwise pay what it can unless that would leave a stub below the minimum, in which case hold enough back that at least the minimum is left to charge. An order that is already under the minimum leaves the card unspent rather than burning it on a payment that cannot complete.The cart and checkout pages print a sentence under the totals saying how much of the card was used and how much stays on it — "Gift card ····ABCD pays $26.12 and holds $0.30 back: a payment cannot be smaller than $0.50, so $0.50 goes on a card" — so the arithmetic on the page is still the arithmetic a shopper can follow. No new dependencies; the security floor and the shape of
src/are unchanged.Worth looking at:
src/money.tsfor the rule and the case where the total itself is under the minimum, and the two new tests —src/units.test.tswalks the boundary (card equal to the total, one cent short, twenty cents short, exactly the minimum, and a sweep assertingdue_centsis 0 or ≥ 50 and that the parts still sum to the total), andsrc/stripe.test.tsteaches the fake Stripe to refuse anything under fifty cents, then drives a real browser flow to a Checkout Session for the fifty cents that were left. Both fail without the change.bun testis green, in order and with--randomize(93 tests). The preview runs the mock gateway, whose minimum is zero, so the held-back case is only visible there with a test key set; on the preview a card short of the total still spends everything, which is what the mock can take.🤖 Generated with Claude Code
Closes #20