A guest keeps a cart and signs in to buy, and from then on the order is theirs: their own /orders list, and their order pages open without a token.
An order today is anonymous — an email typed at checkout and an access_token in the link. That is the whole of the customer's relationship with the shop. With ctx.who in hand (#9) an order can carry the account that placed it, which is what makes "a stranger can buy the one thing" and "the money trail is complete" the same story.
Done looks like
[ ] "order" gains buyer_id and buyer_name, added through addColumn with '' defaults, so the orders already in the database keep rendering and keep their guest story.
[ ] Browsing and the cart stay open to anon: /, /c/:slug, /p/:slug, /cart, adding, updating and applying a code all work signed out, exactly as now.
[ ] GET /checkout for anon renders the same order summary it renders today, with the address form replaced by one Sign in to buy link to /__forge/signin?to=/checkout. The cart cookie is untouched, so the shopper comes back to the same cart.
[ ] POST /checkout refuses anon: no order row, no stock taken, no code redeemed — a 303 back to /checkout. A test proves nothing was written.
[ ] POST /checkout records buyer_id and buyer_name from ctx.who; the email field is prefilled where the forge gave us something to prefill, and the page says which account the order will belong to.
[ ] GET /orders lists the signed-in account's orders, newest first: number, date, status, total, and a link. Anon gets the sign-in invitation, never a list. No account ever sees another's orders.
[ ] /orders/:number, /orders/:number/receipt and /pay/:number open for their buyer with no ?token=. The token keeps working for the guest orders that already exist and for the links in receipts and Stripe return URLs; an admin keeps their access; a signed-in stranger gets the same answer a stranger gets today.
[ ] /admin/orders and /admin/orders/:number show the buyer's account name beside the email.
[ ] Tests: the buyer opens their order with no token; a second account cannot; a cart filled as a guest becomes that account's order after signing in; /orders shows one account only what is theirs.
[ ] README says an account is needed to buy, that browsing and the cart are not, and what the access token is still for.
Files I expect to change
New src/pages/orders.ts (the list) if src/pages/checkout.ts stops fitting in one read. Changed: src/db.ts, src/orders.ts, src/shop.ts, src/routes.ts, src/pages/checkout.ts, src/pages/cart.ts, src/pages/admin.ts, src/admin.ts, src/server.test.ts, README.md.
Risks
Requiring an account to buy is a change of behaviour a shopper should meet early: say it on the cart page, not after they have typed an address.
The access_token does not become dead code. Receipts, the mock gateway's forms and Stripe's success_url/cancel_url all carry it, and the guest orders already in the database have nothing else.
The email on the order is still the address the shopper typed; a forge account name is not an email address and must not be written into the email column.
Ownership is checked in one place — the helper at src/shop.ts:224 — not re-derived per route, or one route will forget.
Checkout's rate limit still applies after the gate moves.
A guest keeps a cart and signs in to buy, and from then on the order is theirs: their own
/orderslist, and their order pages open without a token.An order today is anonymous — an email typed at checkout and an
access_tokenin the link. That is the whole of the customer's relationship with the shop. Withctx.whoin hand (#9) an order can carry the account that placed it, which is what makes "a stranger can buy the one thing" and "the money trail is complete" the same story.Done looks like
"order"gainsbuyer_idandbuyer_name, added throughaddColumnwith''defaults, so the orders already in the database keep rendering and keep their guest story./,/c/:slug,/p/:slug,/cart, adding, updating and applying a code all work signed out, exactly as now.GET /checkoutfor anon renders the same order summary it renders today, with the address form replaced by one Sign in to buy link to/__forge/signin?to=/checkout. The cart cookie is untouched, so the shopper comes back to the same cart.POST /checkoutrefuses anon: no order row, no stock taken, no code redeemed — a 303 back to/checkout. A test proves nothing was written.POST /checkoutrecordsbuyer_idandbuyer_namefromctx.who; the email field is prefilled where the forge gave us something to prefill, and the page says which account the order will belong to.GET /orderslists the signed-in account's orders, newest first: number, date, status, total, and a link. Anon gets the sign-in invitation, never a list. No account ever sees another's orders./orders/:number,/orders/:number/receiptand/pay/:numberopen for their buyer with no?token=. The token keeps working for the guest orders that already exist and for the links in receipts and Stripe return URLs; an admin keeps their access; a signed-in stranger gets the same answer a stranger gets today./admin/ordersand/admin/orders/:numbershow the buyer's account name beside the email./ordersshows one account only what is theirs.Files I expect to change
New
src/pages/orders.ts(the list) ifsrc/pages/checkout.tsstops fitting in one read. Changed:src/db.ts,src/orders.ts,src/shop.ts,src/routes.ts,src/pages/checkout.ts,src/pages/cart.ts,src/pages/admin.ts,src/admin.ts,src/server.test.ts,README.md.Risks
access_tokendoes not become dead code. Receipts, the mock gateway's forms and Stripe'ssuccess_url/cancel_urlall carry it, and the guest orders already in the database have nothing else.emailcolumn.src/shop.ts:224— not re-derived per route, or one route will forget.Blocked by: #9.
Whose brief fits: ledger.