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

A paid order is dispatched: the buyer sees what happens next and the dispatch is a row #89

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pushed. `bun test` — the command `forge.json` names as the check — passes in order and randomized (139 tests, 1275 assertions).

shipped production is serving it deploy ab962da

opened this

Pushed. bun test — the command forge.json names as the check — passes in order and randomized (139 tests, 1275 assertions).

---

An order's life no longer ends at paid. Dispatch is a column and a row rather than a status machine: order.fulfilled_at is added with addColumn (null on every order placed before it, which is the truth about them), and status goes on meaning only where the money stands — paid, partially_refunded and refunded are untouched. Sending a parcel writes one order_event of kind dispatched naming the admin from the actor columns, with the carrier and tracking in its detail, and one letter in the outbox; a new email.kind column tells the receipt from the dispatch letter, so the receipt written at payment is never rewritten and forOrder still means "the receipt".

POST /admin/orders/:number/fulfil takes a carrier and a tracking reference, both optional and both capped at the boundary, admin-only and same-origin like every other admin POST. The UPDATE carries the whole rule in its WHERE (status = 'paid' AND fulfilled_at IS NULL), so two admins clicking at once write one row, one letter and one date between them; the second click is told it had already gone and writes nothing. An order that is not paid is refused in words, and that refusal is itself a row. Cancelling a dispatched order is refused in words — it can be refunded, not cancelled — and refunding one works as before, restocking still the admin's choice. order.fulfilled goes on the bus after the transaction commits, like every other emit, so an open /admin/orders sees it.

Worth looking at in the preview: the buyer's order page, which now opens with the order's life in three steps — placed, paid, dispatched — each with its date or a dash and what happens next in words, the tracking reference among them and escaped (a test dispatches with 1Z<999 and asserts the page carries 1Z&lt;999 and never the raw form). Then /admin/orders, where a second pill tells dispatched from paid at a glance, and the order page, which offers the dispatch form once and afterwards says when it went and with what. The terminal's orders tab says the same in its row and its open detail, and d dispatches there with CARRIER / TRACKING, gated as an admin's key in the one src/desk.ts table the tab bar and the screen's foot are built from.

What I chose not to do: no carrier or tracking columns — nothing computes with them, so they live in the event's detail and are read back through orders.dispatchOf; no dispatched order status, which would have made every existing status check ambiguous; no delivery step, because nothing here could ever know about one. Dispatching a partially_refunded order is refused along with everything else that is not paid, which is what the issue asked for — say the word if a partly-refunded order should still be able to go out. No new dependency. README gains a section on the step and is blunt that nothing is handed to a carrier and no tracking reference is looked up anywhere.

🤖 Generated with Claude Code

Closes #40

commented

main is merged in, the suite is green (163 tests, also in random order), and the branch is pushed.

A paid order is dispatched: order.fulfilled_at (a column, not a status — status goes on meaning where the money is), one order_event of kind dispatched naming the admin with the carrier and tracking in its detail, and one letter in the outbox distinguished from the receipt by the new email.kind. POST /admin/orders/<number>/fulfil is admin-only and same-origin, both fields capped at the boundary, and the rule lives in the UPDATE's WHERE, so two admins clicking together write one row, one letter and one date between them. The buyer's own order page gains a three-step "where this order is" — placed, paid, dispatched — and d at the terminal does the same job as the form.

This push is the merge of main, where three files meant different things in the same places and both meanings are kept: src/orders.ts takes main's imports for the stock race in place plus the import * as mail the second letter needs; src/desk.ts keeps main's giftCardSplit and my dispatch as two functions; src/pages/admin.ts is the interesting one — main made everything below the nav a single data-live region keyed to the order's number, so the dispatch pill, the dispatch form and what it says once the parcel has gone now live inside that region, with order.fulfilled added to the types it asks for. Worth looking at that seam in the preview: open an order in one tab, dispatch it, and the page says so without a reload, while another order's page stays put. One new test covers exactly that — the event names the order and carries nothing else, the shipped wants from static/live.js agrees the keyed region wants it, and the swapped-in region says when it went and with what; it fails if order.fulfilled is removed from the region's types.

Nothing ships, and the README says so plainly: whatever is typed in the tracking box is stored and shown back, escaped, and no carrier is contacted. No new dependencies.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs