WFDASH-WEBAPP/src/lib/server/stock-items-import.test.ts
Dave aed3f5c861 feat: implement discount functionality in POS system
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
2026-09-03 12:19:21 +02:00

305 lines
10 KiB
TypeScript

import { describe, it, expect } from "vitest";
import * as XLSX from "xlsx";
import {
buildBulkImportPlan,
buildStockExportWorkbook,
buildSupplierIdToNameMap,
buildSupplierLookup,
parseStockImportWorkbook,
type ParsedImportRow,
type StockExportRow,
} from "./stock-items-service";
import type { StockItem, Supplier } from "@/lib/types";
function row(rn: number, partial: Partial<ParsedImportRow>): ParsedImportRow {
return { __rowNumber: rn, ...partial };
}
function stockItem(partial: Partial<StockItem>): StockItem {
return {
id: "",
created: "2026-09-01T00:00:00Z",
updated: "2026-09-02T00:00:00Z",
collectionId: "pbc_xxx",
collectionName: "stock_items",
sku: "",
name: "",
description: "",
unit: "",
location: "",
current_qty: 0,
min_qty: 0,
target_qty: 0,
cost_price: 0,
selling_price: 0,
markup: 0,
supplier: "",
is_active: true,
notes: "",
...partial,
} as StockItem;
}
function supplier(id: string, name: string): Supplier {
return { id, suppliername: name } as Supplier;
}
const SUPPLIER_LOOKUP = buildSupplierLookup([
supplier("sup-1", "ACME"),
supplier("sup-2", "Bosch"),
]);
describe("buildSupplierLookup", () => {
it("maps lower-cased trimmed names to ids", () => {
expect(SUPPLIER_LOOKUP.get("acme")).toBe("sup-1");
expect(SUPPLIER_LOOKUP.get("bosch")).toBe("sup-2");
expect(SUPPLIER_LOOKUP.get("nobody")).toBeUndefined();
});
it("buildBulkImportPlan resolves supplier names with surrounding whitespace", () => {
// End-to-end check: an imported row with " ACME " should
// resolve to the same supplier as "ACME". The planner trims
// before lookup, so this works in practice even though the
// raw map only stores the trimmed key.
const plan = buildBulkImportPlan({
rows: [row(1, { sku: "NEW-1", name: "Widget", supplier: " ACME " })],
existingById: new Map(),
existingBySku: new Map(),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.create).toBe(1);
});
it("buildSupplierIdToNameMap reverses the lookup for diff comparisons", () => {
const idToName = buildSupplierIdToNameMap([
supplier("sup-1", "ACME"),
supplier("sup-2", "Bosch"),
]);
expect(idToName.get("sup-1")).toBe("ACME");
expect(idToName.get("sup-2")).toBe("Bosch");
expect(idToName.get("unknown")).toBeUndefined();
});
});
describe("parseStockImportWorkbook", () => {
it("round-trips an exported workbook and produces the same row count", () => {
const bytes = buildStockExportWorkbook({
rows: [
{
id: "si-1",
sku: "WID-1",
name: "Widget",
description: "",
unit: "ea",
supplier: "ACME",
current_qty: 10,
min_qty: 2,
target_qty: 20,
cost_price: 100,
markup: 30,
selling_price: 130,
location: "A1",
notes: "",
is_active: true,
} as StockExportRow,
],
generatedAt: "2026-09-02T12:00:00.000Z",
supplierNames: ["ACME"],
});
const parsed = parseStockImportWorkbook(bytes);
expect(parsed.headers).toContain("id");
expect(parsed.headers).toContain("name");
expect(parsed.rows).toHaveLength(1);
expect(parsed.rows[0]?.sku).toBe("WID-1");
expect(parsed.rows[0]?.supplier).toBe("ACME");
// Number cells come back as strings — the planner coerces them.
expect(parsed.rows[0]?.current_qty).toBe("10");
});
it("throws ImportParseError when the sheet has no data rows", () => {
const wb = XLSX.utils.book_new();
const sheet = XLSX.utils.aoa_to_sheet([["id", "name", "sku"]]);
XLSX.utils.book_append_sheet(wb, sheet, "Stock");
const bytes = XLSX.write(wb, { type: "buffer", bookType: "xlsx" });
expect(() => parseStockImportWorkbook(bytes)).toThrow(/no data rows/);
});
});
describe("buildBulkImportPlan", () => {
it("classifies unchanged rows as noop", () => {
const existing = stockItem({ id: "si-1", sku: "WID-1", name: "Widget", current_qty: 10 });
const plan = buildBulkImportPlan({
rows: [row(1, { id: "si-1", sku: "WID-1", name: "Widget", current_qty: "10" })],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.noop).toBe(1);
expect(plan.plans[0]?.kind).toBe("noop");
});
it("classifies is_active=false on an existing row as soft_delete", () => {
const existing = stockItem({ id: "si-1", sku: "WID-1", name: "Widget" });
const plan = buildBulkImportPlan({
rows: [row(1, { id: "si-1", sku: "WID-1", name: "Widget", is_active: "FALSE" })],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.softDelete).toBe(1);
expect(plan.plans[0]?.kind).toBe("soft_delete");
});
it("classifies a qty change as update with the delta", () => {
const existing = stockItem({ id: "si-1", sku: "WID-1", name: "Widget", current_qty: 10 });
const plan = buildBulkImportPlan({
rows: [row(1, { id: "si-1", sku: "WID-1", name: "Widget", current_qty: "12" })],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.update).toBe(1);
const p = plan.plans[0];
expect(p?.kind).toBe("update");
if (p?.kind === "update") {
expect(p.qtyDelta).toBe(2);
expect(p.costChanged).toBe(false);
}
});
it("classifies a cost change as update with costChanged=true", () => {
const existing = stockItem({ id: "si-1", sku: "WID-1", name: "Widget", cost_price: 100 });
const plan = buildBulkImportPlan({
rows: [row(1, { id: "si-1", sku: "WID-1", name: "Widget", cost_price: "110" })],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.update).toBe(1);
const p = plan.plans[0];
if (p?.kind === "update") {
expect(p.costChanged).toBe(true);
expect(p.qtyDelta).toBe(0);
} else {
throw new Error(`expected update, got ${p?.kind}`);
}
});
it("classifies a new row with a known supplier as create", () => {
const plan = buildBulkImportPlan({
rows: [row(1, { sku: "NEW-1", name: "New Widget", supplier: "ACME" })],
existingById: new Map(),
existingBySku: new Map(),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.create).toBe(1);
expect(plan.plans[0]?.kind).toBe("create");
});
it("classifies a new row with an unknown supplier as error", () => {
const plan = buildBulkImportPlan({
rows: [row(1, { sku: "NEW-1", name: "New Widget", supplier: "UnknownCo" })],
existingById: new Map(),
existingBySku: new Map(),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.error).toBe(1);
expect(plan.plans[0]?.kind).toBe("error");
});
it("classifies a new row with no sku/name as error", () => {
const plan = buildBulkImportPlan({
rows: [row(1, { name: "" })],
existingById: new Map(),
existingBySku: new Map(),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.error).toBe(1);
});
it("falls back to sku match when the row has no id", () => {
const existing = stockItem({ id: "si-1", sku: "WID-1", name: "Widget", current_qty: 5 });
const plan = buildBulkImportPlan({
rows: [row(1, { sku: "WID-1", name: "Widget", current_qty: "6" })],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
});
expect(plan.summary.update).toBe(1);
const p = plan.plans[0];
if (p?.kind === "update") {
expect(p.stockItemId).toBe("si-1");
expect(p.qtyDelta).toBe(1);
} else {
throw new Error(`expected update, got ${p?.kind}`);
}
});
it("REGRESSION: a row whose supplier matches the existing supplier is a noop (not a phantom 'supplier changed' for every row)", () => {
// This is the exact bug we hit on 2026-09-02: the existing
// row's `supplier` field is a 15-char PB relation id, but
// the imported row's `supplier` is a name. Without the
// idToName map, every row's diff shows
// "supplier: <15charid> → <name>"
// even when the supplier is exactly the same.
//
// The fix: compare name-vs-name, not id-vs-name. Pass the
// supplierIdToName map to the planner and verify a no-change
// import shows zero updates.
const idToName = buildSupplierIdToNameMap([supplier("sup-1", "ACME")]);
const existing = stockItem({
id: "si-1",
sku: "WID-1",
name: "Widget",
supplier: "sup-1", // PB relation id
});
const plan = buildBulkImportPlan({
rows: [row(1, {
id: "si-1",
sku: "WID-1",
name: "Widget",
supplier: "ACME", // imported name
})],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
supplierIdToName: idToName,
});
expect(plan.summary.noop).toBe(1);
expect(plan.summary.update).toBe(0);
});
it("REGRESSION: a genuine supplier change (different name) is still detected as an update", () => {
const idToName = buildSupplierIdToNameMap([
supplier("sup-1", "ACME"),
supplier("sup-2", "Bosch"),
]);
const existing = stockItem({
id: "si-1",
sku: "WID-1",
name: "Widget",
supplier: "sup-1",
});
const plan = buildBulkImportPlan({
rows: [row(1, {
id: "si-1",
sku: "WID-1",
name: "Widget",
supplier: "Bosch",
})],
existingById: new Map([["si-1", existing]]),
existingBySku: new Map([["WID-1", existing]]),
supplierLookup: SUPPLIER_LOOKUP,
supplierIdToName: idToName,
});
expect(plan.summary.update).toBe(1);
const p = plan.plans[0];
if (p?.kind === "update") {
const supplierChange = p.changes.find((c) => c.field === "supplier");
expect(supplierChange?.before).toBe("ACME");
expect(supplierChange?.after).toBe("Bosch");
} else {
throw new Error(`expected update, got ${p?.kind}`);
}
});
});