/** Supplier list rules: folding names into one key, payment terms and due dates. Pure — client-safe. */ export type Terms = "" | "cash" | "30" | "60"; export const TERMS: { id: Terms; label: string }[] = [ { id: "cash", label: "Cash" }, { id: "30", label: "30 days" }, { id: "60", label: "60 days" }, ]; export const termsLabel = (t: string) => TERMS.find((x) => x.id === t)?.label ?? "Not set"; export const parseTerms = (v: unknown): Terms => (v === "cash" || v === "30" || v === "60" ? v : ""); export const isAccount = (t: string) => t === "30" || t === "60"; // Company-form words that change between a PO and a till slip but never name a different supplier. const NOISE = new Set(["PTY", "LTD", "LIMITED", "CC", "INC", "THE", "AND"]); /** * "Lube Force Traders cc" → "LUBEFORCETRADERS". Upper-case, punctuation gone, company-form words * dropped. This is the same name typed two ways — it is NOT a fuzzy match: "Your Tyre Shop" and * "Tyre Shop" stay different suppliers. */ export function supplierKey(name: string): string { return (name || "") .toUpperCase() .replace(/&/g, " AND ") .replace(/[^A-Z0-9]+/g, " ") .split(" ") .filter((w) => w && !NOISE.has(w)) .join(""); } /** The day an account purchase falls due: PO date + terms. "" when there are no account terms or no valid date. */ export function dueDate(poDate: string, terms: string): string { if (!isAccount(terms)) return ""; const d = new Date(`${(poDate || "").slice(0, 10)}T00:00:00Z`); if (Number.isNaN(d.getTime())) return ""; d.setUTCDate(d.getUTCDate() + Number(terms)); return d.toISOString().slice(0, 10); }