Rebasely Beta: data may be purged

examples/storefront Public

Markdown · 3.1 KiB · at 5c7d155

id: checkout
type: concept
owner: justin
component: storefront-api
describes:
  - src/checkout.ts
  - src/orders.ts
  - src/payments.ts
  - src/expiry.ts
  - migrations/0003_orders.sql
reviewed: 2026-10-09

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 stock

Stock 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.

Charging

After the batch, checkout asks the payment provider for a PaymentIntent for the order total ([[0005-stripe-payment-intents]]) and stores its 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 the Worker.

If 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.

Retries

Checking 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 orders

An 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 at Stripe. 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.