templates2.Store.Next/lib/cart.ts
w4c-templates bb75e7c742
Some checks are pending
CI / quality (push) Waiting to run
chore(template): publish Store.Next
2026-10-11 23:24:13 +03:00

58 lines
2.4 KiB
TypeScript

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<string, unknown> => !!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 };
}