Orders
Orders track customer purchases through a state machine. Unlike the cart (a cookie), an order row is only created when checkout starts.
Order States
created → payment_pending → paid → shipped → delivered
↓ ↓ ↓
cancelled cancelled cancelled| State | Description |
|---|---|
created | Draft checkout (active=true) |
payment_pending | Checkout submitted, becomes a real order |
paid | Payment received |
shipped | Shipped to customer |
delivered | Received by customer |
cancelled | Order cancelled |
Allowed transitions are enforced by STATE_TRANSITIONS in src/lib/server/services/order-utils.ts. created, payment_pending, and paid can each move to cancelled; shipped can only move to delivered.
Starting Checkout
The cart cookie first touches the database in orderService.startCheckout. It creates (or resumes) a draft order, snapshots the cookie lines into order_lines with prices/names/tax from getCartView, and reserves stock for 15 minutes.
const { order, checkoutToken, isNew, stockErrors } =
await orderService.startCheckout(cartLines, { customerId, checkoutToken });- The draft is
active=true, state=created, identified by thecheckout_tokencookie (theorders.checkoutTokencolumn). - If the cookie no longer matches the draft’s lines, the draft is wiped and rebuilt: one atomic delete plus multi-row insert via the
atomic()helper (src/lib/server/db/index.ts), so a mid-sequence failure can’t leave a half-built draft. (atomic()is the sanctioned multi-statement write path: D1batch()in production, BEGIN/COMMIT on sqlite.db.transactionis not available on theDbtype.) - Quantities are clamped to available stock; anything unavailable is reported in
stockErrorsrather than failing the whole checkout.
Stock reservations are created only at checkout and expire after 15 minutes. There is no background job — expired reservations are cleaned up opportunistically at checkout entry (reservationService.deleteExpired).
State Transitions
await orderService.transitionState(orderId, "payment_pending");
await orderService.transitionState(orderId, "paid");
await orderService.transitionState(orderId, "shipped");
await orderService.transitionState(orderId, "delivered");Key side effects:
payment_pending— the draft becomes a real order (active=false) andorderPlacedAtis set.paid— stock is validated one final time, then promotion usage bumps and stock decrements for tracked variants run as a singleatomic()batch, and reservations are released.cancelled— stock is restored if the order was alreadypaid/shipped, and any reservations are released.
Order Lines
Lines snapshot the product/variant name, SKU, and pricing (including a full tax breakdown) at checkout time:
order.lines = [
{
variantId: 456,
productName: "Blue Shirt",
variantName: "Medium",
sku: "SHIRT-M",
quantity: 2,
unitPrice: 2999,
lineTotal: 5998,
taxCode: "standard",
taxRate: 2550, // basis points, 4 decimals (0.255)
taxAmount: 1218,
unitPriceNet: 2390,
lineTotalNet: 4780
}
];Totals
Totals math (subtotal, discount split, shipping, tax, net) is the pure calculateOrderTotals in src/lib/server/services/order-utils.ts, fed by recalculateTotals with the current lines, applied promotions, and shipping price. Free-shipping promotion rows are kept in sync with the shipping price on every recalculation.
Order Code
Orders have a human-readable code like ORD-XXXXXXXX:
const order = await orderService.getByCode("ORD-AB12CD34");Guest Orders
Orders can be placed without an account. Guest checkouts are tracked by the checkout_token cookie and store customerEmail directly on the order.
currencyCode and exchangeRate columns exist on orders, but they are vestigial — EUR is hardcoded and no currency conversion is performed.