export type CartLine = { productId: string; variant?: string; quantity: number }; export type Variant = { name: string; priceDelta: number }; export type ProductLike = { id: string; name: string; price: number; stock: number; active?: boolean; variants?: unknown; }; export type PricedItem = { productId: string; name: string; variant?: string; price: number; quantity: number }; export type ValidationResult = { ok: true; items: PricedItem[]; total: number } | { ok: false; error: string }; export function parseVariants(value: unknown): Variant[] { if (!Array.isArray(value)) return []; return value .filter((v): v is Record => !!v && typeof v === 'object') .map((v) => ({ name: String(v.name ?? ''), priceDelta: Number(v.priceDelta ?? 0) })) .filter((v) => v.name.length > 0); } export function unitPrice(product: ProductLike, variant?: string): number | null { if (!variant) return product.price; const match = parseVariants(product.variants).find((v) => v.name === variant); return match ? product.price + match.priceDelta : null; } /** * Validate cart lines against authoritative product data (never trust client prices) and compute * the order total. Pure so it is unit-testable and usable on both server and client. */ export function validateLines(lines: CartLine[], products: ProductLike[]): ValidationResult { if (!Array.isArray(lines) || lines.length === 0) return { ok: false, error: 'Your cart is empty.' }; const byId = new Map(products.map((p) => [p.id, p])); const items: PricedItem[] = []; let total = 0; for (const line of lines) { const product = byId.get(line.productId); if (!product || product.active === false) { return { ok: false, error: 'A product in your cart is no longer available.' }; } const quantity = Math.floor(Number(line.quantity)); if (!Number.isFinite(quantity) || quantity < 1) return { ok: false, error: 'Invalid quantity.' }; const price = unitPrice(product, line.variant); if (price === null) return { ok: false, error: `Unknown option "${line.variant}" for ${product.name}.` }; if (quantity > product.stock) return { ok: false, error: `Only ${product.stock} left of ${product.name}.` }; items.push({ productId: product.id, name: product.name, variant: line.variant, price, quantity }); total += price * quantity; } return { ok: true, items, total }; }