import { discountPercent } from "./format"; import type { Product } from "./payload"; // Previously duplicated independently in CartContent.tsx, CheckoutContent.tsx, // and BestellbestaetigungContent.tsx — pulled into one place now that adding // discount-code math to all three at once would otherwise mean hand-editing // 3 near-identical blocks (and risking them drifting apart). export type CartLine = { entry: { qty: number; variant?: string }; product: Product }; export type DiscountLike = { type: "percent" | "fixed"; value: number }; // A selected variant's priceOverride wins over the base product price — // null/undefined priceOverride (or no variant selected at all) falls back // to it. The one place cart/checkout math needs to know about variants at // all; every other total below builds on this instead of `product.price` // directly. export function effectivePrice(entry: { variant?: string }, product: Product): number { if (!entry.variant) return product.price; const variant = product.variants.find((v) => v.name === entry.variant); return variant?.priceOverride ?? product.price; } // A product's own taxRatePercent override wins over the tenant's default // rate — mirrors api/checkout/route.ts's server-side snapshot logic // (`product.taxRatePercent ?? defaultTaxRate`), kept in sync deliberately // since this is only ever used for display, never for the actual charged // amount. export function effectiveTaxRate(product: Product, defaultRate: number): number { return product.taxRatePercent ?? defaultRate; } // Split out from computeCartTotals() below because callers need a subtotal // figure *before* they can decide a shipping cost (e.g. checking it against // a free-shipping threshold) — which computeCartTotals itself takes as an // input, not something it can decide on its own. export function computeSubtotal(items: CartLine[]): number { return items.reduce((sum, { entry, product }) => sum + entry.qty * effectivePrice(entry, product), 0); } // A cart only needs a shipping line at all if at least one item doesn't // opt out via Products.noShippingCost (e.g. a purely digital download) — // mirrors api/checkout/route.ts's own hasShippableItem check, which is // the actual charged amount; this is only the storefront's estimate/ // display before that. A single non-exempt item still triggers normal // shipping for the whole cart, this never partially discounts it. export function cartHasShippableItem(items: CartLine[]): boolean { return items.some(({ product }) => !product.noShippingCost); } export type CartTotals = { subtotal: number; /** compareAtPrice-based per-product savings — already excluded from * `subtotal` (which uses `price`, not `compareAtPrice`), this is a * separate display line only. */ totalSavings: number; discountAmount: number; total: number; }; export function computeCartTotals(items: CartLine[], shippingCost: number, discount?: DiscountLike | null): CartTotals { const subtotal = computeSubtotal(items); const totalSavings = items.reduce((sum, { entry, product }) => { const productDiscountPct = discountPercent(product.price, product.compareAtPrice); return productDiscountPct !== null ? sum + entry.qty * (product.compareAtPrice! - product.price) : sum; }, 0); const discountAmount = !discount ? 0 : discount.type === "percent" ? (subtotal * discount.value) / 100 : Math.min(discount.value, subtotal); const total = Math.max(0, subtotal - discountAmount) + shippingCost; return { subtotal, totalSavings, discountAmount, total }; }