examples/storefront Public
// Orders: what a cart becomes at checkout. An order starts as pending_payment and moves on when// the payment provider reports the outcome.import type { Totals } from "./money.ts";export type OrderStatus = "pending_payment" | "paid" | "cancelled" | "fulfilled";export type Address = { name: string; line1: string; line2: string; city: string; postal_code: string; country: string;};export type OrderItem = { product_id: string; name: string; quantity: number; unit_price_cents: number };export type Order = { id: string; status: OrderStatus; email: string; ship_to: Address; items: OrderItem[]; created_at: string;} & Totals;type Row = { id: string; status: OrderStatus; email: string; ship_to: string; subtotal_cents: number; shipping_cents: number; total_cents: number; currency: string; created_at: number;};export async function getOrder(db: D1Database, id: string): Promise<Order | null> { const row = await db .prepare("SELECT id, status, email, ship_to, subtotal_cents, shipping_cents, total_cents, currency, created_at FROM orders WHERE id = ?") .bind(id) .first<Row>(); if (!row) return null; const { results } = await db .prepare("SELECT product_id, name, quantity, unit_price_cents FROM order_items WHERE order_id = ? ORDER BY product_id") .bind(id) .all<OrderItem>(); return { id: row.id, status: row.status, email: row.email, ship_to: JSON.parse(row.ship_to) as Address, items: results, subtotal_cents: row.subtotal_cents, shipping_cents: row.shipping_cents, total_cents: row.total_cents, currency: row.currency, created_at: new Date(row.created_at).toISOString(), };}// What the payment side learns back: the order's status after the call, and whether the call// changed it. Both calls are idempotent, because Stripe delivers webhooks at least once.export type Settled = { order_id: string; status: OrderStatus; changed: boolean };export async function markPaid(db: D1Database, orderId: string, paymentIntentId: string, now: number): Promise<Settled> { const r = await db .prepare("UPDATE orders SET status = 'paid', updated_at = ? WHERE id = ? AND payment_intent_id = ? AND status = 'pending_payment'") .bind(now, orderId, paymentIntentId) .run(); return settled(db, orderId, r.meta.changes > 0);}// Cancels an order that is still waiting for payment and puts its stock back, in one batch. The// stock update runs first and only while the order is pending, so a second call changes nothing.export async function cancelOrder(db: D1Database, orderId: string, now: number): Promise<Settled> { const [, cancel] = await db.batch([ db .prepare( `UPDATE products SET stock = stock + (SELECT quantity FROM order_items WHERE order_id = ?1 AND product_id = products.id), updated_at = ?2 WHERE id IN (SELECT product_id FROM order_items WHERE order_id = ?1) AND EXISTS (SELECT 1 FROM orders WHERE id = ?1 AND status = 'pending_payment')`, ) .bind(orderId, now), db.prepare("UPDATE orders SET status = 'cancelled', updated_at = ? WHERE id = ? AND status = 'pending_payment'").bind(now, orderId), ]); return settled(db, orderId, (cancel?.meta.changes ?? 0) > 0);}async function settled(db: D1Database, orderId: string, changed: boolean): Promise<Settled> { const status = await db.prepare("SELECT status FROM orders WHERE id = ?").bind(orderId).first<OrderStatus>("status"); if (!status) throw new Error(`no order ${orderId}`); return { order_id: orderId, status, changed };}