58 lines
2.4 KiB
TypeScript
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 };
|
|
}
|