Rebasely Beta: data may be purged

examples/storefront Public

Markdown · 52 lines · 3.3 KiB · at e83464e

---id: checkouttype: conceptowner: justincomponent: storefront-apidescribes:  - src/checkout.ts  - src/orders.ts  - src/payments.ts  - src/expiry.ts  - migrations/0003_orders.sqlreviewed: 2026-10-15---# Checkout`POST /carts/:id/checkout` with an email and a shipping address turns an open cart into an order and returns what the browser needs to take the payment. The steps are in the checkout flow on the Architecture tab.## Taking stockStock is taken here and nowhere else ([[0004-take-stock-at-checkout]]). The order, its lines, one `UPDATE products SET stock = stock - n` per line and closing the cart go to D1 as one batch. `products.stock` has `CHECK (stock >= 0)`, so when two shoppers race for the last pair of socks, the second batch fails as a whole and that shopper gets `409 out_of_stock` with what is left of each item. A first pass checks stock before the batch so the common case gets the same answer without a failed write.Lines keep the price that was in the cart. The shopper pays what the cart showed them.## ChargingAfter the batch, checkout calls `PaymentsService.createIntent` on the payments Worker over the `PAYMENTS` service binding ([[payments]]), which creates a Stripe PaymentIntent for the order total, and stores the intent's id on the order. The response has the order and the intent's `client_secret`; the browser confirms the payment with Stripe directly, so card details never reach our Workers, and storefront-api never holds a Stripe key ([[0010-payments-worker]]).If the payments Worker or Stripe fails or times out, nothing has been charged, so the checkout is undone in one batch: the stock goes back, the order is deleted and the cart opens again. The shopper gets `502 payments_unavailable` and can retry.## RetriesChecking out the same cart again while its order is waiting for payment returns that order and the same client secret: the intent is created with the idempotency key `order-<id>`, so Stripe returns the existing one. Once the order is paid or cancelled, the cart answers `409 cart_closed`.## Unpaid ordersAn order holds its stock until it is paid or cancelled, so unpaid ones expire ([[0009-cancel-unpaid-orders-after-30-minutes]]). Every five minutes the cron trigger runs `expireOrders` (`src/expiry.ts`) over up to 50 orders that have been `pending_payment` for more than 30 minutes:1. Cancel the PaymentIntent through `PaymentsService.cancelIntent`. If Stripe refuses because the intent succeeded or a payment is processing, the payment won the race: leave the order to the webhook.2. Otherwise cancel the order, which puts the stock back and sends `order.cancelled`.Cancelling the intent first means a shopper cannot pay for an order after it has been cancelled.## Order states| Status | Meaning || --- | --- || `pending_payment` | Created at checkout; stock is held for it || `paid` | Stripe reported the payment succeeded ([[payment-webhook]]) || `cancelled` | Not paid; the stock went back in the same batch || `fulfilled` | Handed to the warehouse ([[fulfilment]]) |State changes after checkout are in `src/orders.ts` (`markPaid`, `cancelOrder`) and only move forward from `pending_payment`, so repeating one is harmless. Each change is announced on the queue ([[order-events]]). `GET /orders/:id` returns the order. The id is a random UUID that only the shopper who checked out has.