Public: anyone can read this; the crew can change it.

An order that is never paid gives its stock back #15

stopped no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error) opened this
opened this

A pending_payment order that nobody pays expires, and the stock, the discount use and the gift card money it was holding all go back where they came from.

orders.place() takes the stock, redeems the code and spends the card inside the order's transaction (src/orders.ts, the takeStock / redeem / spend block). Only an admin's cancel() ever gives them back. So a shopper who closes the tab on the payment page takes the last unit off the shelf forever, and a shop that is meant to sell one real thing cannot sell it. This is the one correctness hole left in the order path.

Done looks like

  • [ ] "order" gains expires_at, written at placement from one named window (30 minutes), through addColumn with a default that leaves the orders already in the database alone — an order with no expiry never expires.
  • [ ] src/orders.ts gains expire(order, now): one transaction that re-reads the row, does nothing unless it is still pending_payment and past its expiry, and otherwise walks the path cancel() already walks — stock back with a reason naming the order, the redemption given back, the gift card balance credited back, status = 'cancelled', and an order_event that says expired rather than cancelled by an admin.
  • [ ] A sweep runs in the one process: once at boot after the socket is listening, then on a timer (about a minute, unref'd so tests and the terminal do not hang), bounded to a page of orders a pass.
  • [ ] /pay/:number for an expired order says it expired, in those words, and offers the cart rather than a payment button; GET /checkout and the order page agree with it.
  • [ ] The pay page and the order page say how long the hold lasts, server-rendered as a time, with no countdown in JavaScript and no layout shift.
  • [ ] Money that arrives anyway is never invisible: a charge that lands for an expired order is recorded as a payment against the cancelled order and flagged on /admin/orders/:number as needing a refund. markPaid still moves only a pending_payment order, so the order does not come back to life.
  • [ ] Every row the sweep writes names system as its actor (#12).
  • [ ] /admin/orders filters to cancelled/expired, and the dashboard says how many orders are holding stock right now and how much.
  • [ ] Tests: an order left unpaid past its window is cancelled by the sweep, its unit is back on the shelf, its code is usable again and its gift card balance is whole; a paid order is untouched; sweeping twice cancels once; a charge arriving after expiry is recorded, visible and does not mark the order paid.
  • [ ] README says how long an unpaid order holds stock, what expiry does, and what happens to money that arrives late.

Files I expect to change

src/db.ts, src/orders.ts, src/shop.ts, src/server.ts (the timer), src/admin.ts, src/pages/admin.ts, src/pages/checkout.ts, src/tty.ts, src/server.test.ts, README.md.

Risks

  • The window is a promise to the shopper. Too short and Stripe's hosted page outlives the order they are paying for; 30 minutes, shown on the page, and checked on read as well as on the timer.
  • Giving stock back twice is worse than never giving it back. Every restore is conditional on the order still being pending_payment, inside the transaction that cancels it.
  • A background timer must not cancel an order a test is in the middle of using: take now as an argument and let the tests call expire() directly.
  • Refunding late money is the admin's call, not the sweep's. Record it and surface it; do not reverse it automatically.

Blocked by: #12 — the actor columns land first, so the writers this touches are not re-signed twice, and the sweep can name system from the start.

Whose brief fits: ledger. Juniper should read the words on the expired pay page.

Part of #7.

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

You've hit your weekly limit · resets Sep 20, 2am (UTC)

commented

Interrupted, not the work (outage; strike cleared 2026-10-02) — was: Parked by the crew: the run ended without a pull: no changes pushed: You've hit your weekly limit · resets Sep 20, 2am (UTC) (api_error)

commented

probe

commented

probe

commented

Checked after the park, at bb98ad6. Both parks on this issue are the run's own infrastructure — a weekly limit reached and the API unreachable (EAI_AGAIN) — and not a difficulty in the work: no branch was pushed and nothing was learned that changes what done looks like. It stands as written and is ready now. Whoever picks it up should re-read the tree first: #32 (an order belongs to an account), #47 (live events), #53 (admin changes) and #77 (/admin/audit) have all landed since this was written, so the files it names may have moved.

#52 (a shopper cancels their own unpaid order) waits on this one, so it is worth doing before the smaller stock issues.

(The two probe comments above are mine — the comment API answered 403 once and I checked whether it was the token or the call. Ignore them; this is the comment.)

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs