/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; interface CartItemInput { id?: string; name?: string; price?: number; quantity?: number; category?: string; } interface DiscountRuleInput { id?: string; label?: string; category?: string; threshold?: number; percent?: number; } interface ShoppingCartArgs { items: Default; discounts: Default; } interface CartItem { id: string; name: string; price: number; quantity: number; category: string; } interface DiscountRule { id: string; label: string; category: string; threshold: number; percent: number; } interface LineTotal { id: string; name: string; category: string; unitPrice: number; quantity: number; subtotal: number; } interface CategoryTotal { category: string; quantity: number; subtotal: number; } interface DiscountApplication { id: string; label: string; category: string; threshold: number; percent: number; qualified: boolean; amount: number; } type CartEvent = | { type: "add"; item?: CartItemInput } | { type: "update"; id?: string; quantity?: number; price?: number } | { type: "remove"; id?: string } | { type: "clear" }; type DiscountEvent = | { type?: "replace"; rules?: DiscountRuleInput[] } | { type: "clear" }; const defaultItemName = "Item"; const defaultCategory = "general"; const clampNumber = (value: number, min: number, max: number): number => { if (Number.isNaN(value)) return min; if (value < min) return min; if (value > max) return max; return value; }; const normalizeString = (value: unknown): string | null => { if (typeof value !== "string") return null; const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; }; const claimIdentifier = (base: string, used: Set): string => { let candidate = base; let suffix = 1; while (used.has(candidate)) { candidate = `${base}-${suffix}`; suffix += 1; } return candidate; }; const sanitizeName = (value: unknown, index: number): string => { const normalized = normalizeString(value); if (normalized) return normalized; return `${defaultItemName} ${index + 1}`; }; const sanitizeCategory = (value: unknown): string => { const normalized = normalizeString(value)?.toLowerCase(); if (normalized) return normalized; return defaultCategory; }; const sanitizePrice = (value: unknown): number => { if (typeof value === "number" && Number.isFinite(value)) { return clampNumber(Math.round(value * 100) / 100, 0, 1_000_000); } return 0; }; const sanitizeQuantity = (value: unknown): number => { if (typeof value === "number" && Number.isFinite(value)) { const rounded = Math.trunc(value); return clampNumber(rounded, 0, 10_000); } return 1; }; const sanitizePercent = (value: unknown): number => { if (typeof value === "number" && Number.isFinite(value)) { return clampNumber(Math.round(value * 100) / 100, 0, 100); } return 0; }; const sanitizeThreshold = (value: unknown): number => { if (typeof value === "number" && Number.isFinite(value)) { const rounded = Math.trunc(value); return clampNumber(rounded, 1, 10_000); } return 1; }; const sanitizeCartItem = ( input: CartItemInput | undefined, index: number, used: Set, ): CartItem => { const providedId = normalizeString(input?.id)?.toLowerCase(); const fallback = `item-${index + 1}`; const id = providedId ?? claimIdentifier(fallback, used); const name = sanitizeName(input?.name ?? id, index); const price = sanitizePrice(input?.price); const quantity = sanitizeQuantity(input?.quantity); const category = sanitizeCategory(input?.category); return { id, name, price, quantity, category }; }; const sanitizeCartItems = ( entries: readonly CartItemInput[] | undefined, ): CartItem[] => { const result: CartItem[] = []; const list = Array.isArray(entries) ? entries : []; const used = new Set(); for (let index = 0; index < list.length; index++) { const sanitized = sanitizeCartItem(list[index], index, used); used.add(sanitized.id); result.push(sanitized); } return result; }; const sanitizeDiscountRule = ( input: DiscountRuleInput | undefined, index: number, used: Set, ): DiscountRule => { const providedId = normalizeString(input?.id)?.toLowerCase(); const fallback = `rule-${index + 1}`; const id = providedId ?? claimIdentifier(fallback, used); const label = sanitizeName(input?.label ?? id, index); const category = sanitizeCategory(input?.category); const threshold = sanitizeThreshold(input?.threshold); const percent = sanitizePercent(input?.percent); return { id, label, category, threshold, percent }; }; const sanitizeDiscountRules = ( entries: readonly DiscountRuleInput[] | undefined, ): DiscountRule[] => { const result: DiscountRule[] = []; const list = Array.isArray(entries) ? entries : []; const used = new Set(); for (let index = 0; index < list.length; index++) { const sanitized = sanitizeDiscountRule(list[index], index, used); used.add(sanitized.id); result.push(sanitized); } return result; }; const roundCurrency = (value: number): number => { return Math.round(value * 100) / 100; }; const formatCurrency = (value: number): string => { return `$${roundCurrency(value).toFixed(2)}`; }; const computeLineTotals = (items: CartItem[]): LineTotal[] => { return items.map((item) => { const subtotal = roundCurrency(item.price * item.quantity); return { id: item.id, name: item.name, category: item.category, unitPrice: item.price, quantity: item.quantity, subtotal, }; }); }; const computeCategoryTotals = (items: CartItem[]): CategoryTotal[] => { const map = new Map(); for (const item of items) { const entry = map.get(item.category) ?? { quantity: 0, subtotal: 0 }; entry.quantity += item.quantity; entry.subtotal = roundCurrency(entry.subtotal + item.price * item.quantity); map.set(item.category, entry); } return Array.from(map.entries()) .sort((a, b) => a[0].localeCompare(b[0])) .map(([category, entry]) => ({ category, quantity: entry.quantity, subtotal: roundCurrency(entry.subtotal), })); }; const computeDiscountApplications = ( rules: DiscountRule[], totals: CategoryTotal[], ): DiscountApplication[] => { const index = new Map(); for (const entry of totals) { index.set(entry.category, entry); } return rules.map((rule) => { const bucket = index.get(rule.category); if (!bucket) { return { ...rule, qualified: false, amount: 0 }; } const qualified = bucket.quantity >= rule.threshold && rule.percent > 0; if (!qualified) { return { ...rule, qualified, amount: 0 }; } const raw = (bucket.subtotal * rule.percent) / 100; return { ...rule, qualified, amount: roundCurrency(raw) }; }); }; const sumLineSubtotal = (lines: LineTotal[]): number => { let total = 0; for (const line of lines) { total = roundCurrency(total + line.subtotal); } return total; }; const sumLineQuantity = (lines: LineTotal[]): number => { let total = 0; for (const line of lines) { total += line.quantity; } return total; }; const sumDiscountAmount = (entries: DiscountApplication[]): number => { let total = 0; for (const entry of entries) { total = roundCurrency(total + entry.amount); } return total; }; const modifyCart = handler( ( event: CartEvent | undefined, context: { items: Cell; history: Cell }, ) => { if (!event || typeof event !== "object") return; const current = sanitizeCartItems(context.items.get()); const used = new Set(current.map((item) => item.id)); const existingHistory = context.history.get(); const history = Array.isArray(existingHistory) ? [...existingHistory] : []; switch (event.type) { case "add": { const sanitized = sanitizeCartItem(event.item, current.length, used); const index = current.findIndex((item) => item.id === sanitized.id); const next = index >= 0 ? current.map((item, position) => position === index ? sanitized : item ) : [...current, sanitized]; const amountLabel = `${sanitized.quantity} x ` + `${formatCurrency(sanitized.price)}`; const message = index >= 0 ? `Replaced ${sanitized.id} with ${amountLabel}` : `Added ${sanitized.id} with ${amountLabel}`; context.items.set(next); context.history.set([...history, message]); break; } case "update": { const id = normalizeString(event.id)?.toLowerCase(); if (!id) return; const index = current.findIndex((item) => item.id === id); if (index === -1) return; const existing = current[index]; const quantity = event.quantity === undefined ? existing.quantity : sanitizeQuantity(event.quantity); const price = event.price === undefined ? existing.price : sanitizePrice(event.price); const updated: CartItem = { ...existing, quantity, price, }; const next = current.map((item, position) => { return position === index ? updated : item; }); const message = `Updated ${updated.id} to ${updated.quantity} x ` + `${formatCurrency(updated.price)}`; context.items.set(next); context.history.set([...history, message]); break; } case "remove": { const id = normalizeString(event.id)?.toLowerCase(); if (!id) return; if (!current.some((item) => item.id === id)) return; const next = current.filter((item) => item.id !== id); const message = `Removed ${id} from cart`; context.items.set(next); context.history.set([...history, message]); break; } case "clear": { if (current.length === 0) return; context.items.set([]); context.history.set([...history, "Cleared cart items"]); break; } } }, ); const configureDiscounts = handler( ( event: DiscountEvent | undefined, context: { discounts: Cell; history: Cell }, ) => { if (!event || typeof event !== "object") return; const history = Array.isArray(context.history.get()) ? [...context.history.get()] : []; if (event.type === "clear") { context.discounts.set([]); context.history.set([...history, "Cleared discounts"]); return; } const rules = sanitizeDiscountRules(event.rules); context.discounts.set(rules); const message = `Configured ${rules.length} discount rule(s)`; context.history.set([...history, message]); }, ); export const shoppingCartAggregation = recipe( "Shopping Cart Aggregation", ({ items, discounts }) => { const cartItems = lift(sanitizeCartItems)(items); const discountRules = lift(sanitizeDiscountRules)(discounts); const categoryTotals = lift(computeCategoryTotals)(cartItems); const lineTotals = lift(computeLineTotals)(cartItems); const subtotal = lift(sumLineSubtotal)(lineTotals); const itemCount = lift(sumLineQuantity)(lineTotals); const discountApplications = lift((input: { rules: DiscountRule[]; totals: CategoryTotal[]; }) => computeDiscountApplications(input.rules, input.totals))({ rules: discountRules, totals: categoryTotals, }); const totalDiscount = lift(sumDiscountAmount)(discountApplications); const grandTotal = lift((input: { subtotal: number; discount: number }) => roundCurrency(Math.max(0, input.subtotal - input.discount)) )({ subtotal, discount: totalDiscount, }); const subtotalDisplay = lift(formatCurrency)(subtotal); const discountDisplay = lift(formatCurrency)(totalDiscount); const totalDisplay = lift(formatCurrency)(grandTotal); const history = cell([]); const lastEvent = lift((input: { log: string[]; lines: LineTotal[] }) => { if (!Array.isArray(input.log) || input.log.length === 0) { const count = Array.isArray(input.lines) ? input.lines.length : 0; return `Cart initialized with ${count} item(s)`; } return input.log[input.log.length - 1]; })({ log: history, lines: lineTotals, }); const summary = str`Cart subtotal ${subtotalDisplay} • discount ${discountDisplay} • total ${totalDisplay}`; return { items: cartItems, discountRules, categoryTotals, lineTotals, discountBreakdown: discountApplications, subtotal, itemCount, totalDiscount, total: grandTotal, summary, history, lastEvent, modify: modifyCart({ items, history }), configureDiscounts: configureDiscounts({ discounts, history }), }; }, );