Rebasely Beta: data may be purged

examples/storefront Public

TypeScript · 114 lines · 4.7 KiB · at c8458cc

// 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 { OrderEvent } from "./events.ts";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 };// Marks a pending order paid and announces it on the order-events queue. The event is sent// whenever the order ends up paid, even if an earlier call already marked it: if that call's// send failed, Stripe's retry is what gets the event out. Consumers dedupe by order id.export async function markPaid(db: D1Database, events: Queue<OrderEvent>, 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();  const result = await settled(db, orderId, r.meta.changes > 0);  if (result.status === "paid") await events.send({ type: "order.paid", order_id: orderId, at: new Date(now).toISOString() });  return result;}// 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, events: Queue<OrderEvent>, 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),  ]);  const result = await settled(db, orderId, (cancel?.meta.changes ?? 0) > 0);  if (result.changed) await events.send({ type: "order.cancelled", order_id: orderId, at: new Date(now).toISOString() });  return result;}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 };}// Records the warehouse's shipment for a paid order. A second call for the same order changes// nothing and reports the order as it is.export async function recordShipment(db: D1Database, orderId: string, shipmentId: string, now: number): Promise<Settled> {  const r = await db    .prepare("UPDATE orders SET status = 'fulfilled', shipment_id = ?, fulfilled_at = ?, updated_at = ? WHERE id = ? AND status = 'paid'")    .bind(shipmentId, now, now, orderId)    .run();  return settled(db, orderId, r.meta.changes > 0);}