examples/storefront Public
// Carts: a shopper's items before checkout. A cart is open until it is checked out.import { HttpError } from "./http.ts";import { totals, type Totals } from "./money.ts";export const MAX_QUANTITY = 10;export const MAX_LINES = 50;export type CartItem = { product_id: string; name: string; quantity: number; unit_price_cents: number; image_url: string | null;};export type Cart = { id: string; status: "open" | "checked_out"; items: CartItem[] } & Totals;type ItemRow = { product_id: string; name: string; quantity: number; unit_price_cents: number; image_key: string | null };export async function createCart(db: D1Database, id: string, now: number): Promise<Cart> { await db.prepare("INSERT INTO carts (id, created_at, updated_at) VALUES (?, ?, ?)").bind(id, now, now).run(); return (await getCart(db, id))!;}export async function getCart(db: D1Database, id: string): Promise<Cart | null> { const cart = await db.prepare("SELECT id, status FROM carts WHERE id = ?").bind(id).first<{ id: string; status: Cart["status"] }>(); if (!cart) return null; const { results } = await db .prepare( `SELECT ci.product_id, p.name, ci.quantity, ci.unit_price_cents, p.image_key FROM cart_items ci JOIN products p ON p.id = ci.product_id WHERE ci.cart_id = ? ORDER BY ci.added_at, ci.product_id`, ) .bind(id) .all<ItemRow>(); const items = results.map((r) => ({ product_id: r.product_id, name: r.name, quantity: r.quantity, unit_price_cents: r.unit_price_cents, image_url: r.image_key ? `/images/${r.image_key}` : null, })); return { id: cart.id, status: cart.status, items, ...totals(items) };}async function openCart(db: D1Database, id: string): Promise<void> { const cart = await db.prepare("SELECT status FROM carts WHERE id = ?").bind(id).first<{ status: string }>(); if (!cart) throw new HttpError(404, "cart_not_found", "No such cart."); if (cart.status !== "open") throw new HttpError(409, "cart_closed", "This cart has been checked out.");}// Sets how many of a product the cart holds; 0 removes it. The price is the product's price now.export async function setQuantity(db: D1Database, cartId: string, productId: string, quantity: unknown, now: number): Promise<Cart> { if (!Number.isInteger(quantity) || (quantity as number) < 0 || (quantity as number) > MAX_QUANTITY) { throw new HttpError(400, "bad_quantity", `quantity is a whole number from 0 to ${MAX_QUANTITY}.`); } await openCart(db, cartId); if (quantity === 0) { await db.prepare("DELETE FROM cart_items WHERE cart_id = ? AND product_id = ?").bind(cartId, productId).run(); } else { const product = await db .prepare("SELECT price_cents, stock FROM products WHERE id = ? AND active = 1") .bind(productId) .first<{ price_cents: number; stock: number }>(); if (!product) throw new HttpError(404, "product_not_found", "No such product."); if (product.stock < (quantity as number)) { throw new HttpError(409, "out_of_stock", "There are not that many left.", { product_id: productId, available: product.stock }); } const lines = await db .prepare("SELECT COUNT(*) AS n FROM cart_items WHERE cart_id = ? AND product_id != ?") .bind(cartId, productId) .first<number>("n"); if ((lines ?? 0) >= MAX_LINES) throw new HttpError(409, "cart_full", `A cart holds at most ${MAX_LINES} different products.`); await db .prepare( `INSERT INTO cart_items (cart_id, product_id, quantity, unit_price_cents, added_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT (cart_id, product_id) DO UPDATE SET quantity = excluded.quantity, unit_price_cents = excluded.unit_price_cents`, ) .bind(cartId, productId, quantity, product.price_cents, now) .run(); } await db.prepare("UPDATE carts SET updated_at = ? WHERE id = ?").bind(now, cartId).run(); return (await getCart(db, cartId))!;}