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.
153 lines
4.5 KiB
TypeScript
153 lines
4.5 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import * as XLSX from "xlsx";
|
|
import {
|
|
buildStockExportWorkbook,
|
|
STOCK_EXPORT_COLUMNS,
|
|
stockItemToExportRow,
|
|
type StockExportRow,
|
|
} from "./stock-items-service";
|
|
|
|
function parseWorkbook(bytes: Uint8Array): { sheets: Record<string, XLSX.WorkSheet> } {
|
|
const wb = XLSX.read(bytes, { type: "buffer" });
|
|
return { sheets: wb.Sheets };
|
|
}
|
|
|
|
describe("stockItemToExportRow", () => {
|
|
it("flattens a StockItem to the canonical export row, resolving supplier to name", () => {
|
|
const row = stockItemToExportRow(
|
|
{
|
|
id: "si-1",
|
|
sku: "WID-1",
|
|
name: "Widget",
|
|
description: "A widget",
|
|
unit: "ea",
|
|
current_qty: 10,
|
|
min_qty: 2,
|
|
target_qty: 20,
|
|
cost_price: 100,
|
|
markup: 30,
|
|
selling_price: 130,
|
|
location: "A1",
|
|
notes: "shiny",
|
|
is_active: true,
|
|
supplier: "sup-1",
|
|
created: "2026-09-01T00:00:00Z",
|
|
updated: "2026-09-02T00:00:00Z",
|
|
collectionId: "pbc_xxx",
|
|
collectionName: "stock_items",
|
|
} as never,
|
|
"ACME",
|
|
);
|
|
expect(row.id).toBe("si-1");
|
|
expect(row.sku).toBe("WID-1");
|
|
expect(row.supplier).toBe("ACME");
|
|
expect(row.is_active).toBe(true);
|
|
expect(row.current_qty).toBe(10);
|
|
expect(row.cost_price).toBe(100);
|
|
});
|
|
|
|
it("treats missing fields as empty / zero / inactive=false (for is_active) sensibly", () => {
|
|
const row = stockItemToExportRow(
|
|
{
|
|
id: "si-2",
|
|
sku: "WID-2",
|
|
name: "Bare",
|
|
is_active: false,
|
|
} as never,
|
|
"",
|
|
);
|
|
expect(row.is_active).toBe(false);
|
|
expect(row.supplier).toBe("");
|
|
expect(row.current_qty).toBe(0);
|
|
expect(row.notes).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("buildStockExportWorkbook", () => {
|
|
const baseRows: StockExportRow[] = [
|
|
{
|
|
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,
|
|
},
|
|
];
|
|
|
|
it("produces a non-empty xlsx that opens in SheetJS and contains the canonical column order", () => {
|
|
const bytes = buildStockExportWorkbook({
|
|
rows: baseRows,
|
|
generatedAt: "2026-09-02T12:00:00.000Z",
|
|
supplierNames: ["ACME", "Bosch"],
|
|
});
|
|
expect(bytes.byteLength).toBeGreaterThan(0);
|
|
const { sheets } = parseWorkbook(bytes);
|
|
const stock = sheets.Stock;
|
|
expect(stock).toBeDefined();
|
|
// Header row is the first row of AOA; columns come back in order.
|
|
const header = (XLSX.utils.sheet_to_json(stock, { header: 1 }) as unknown[][])[0];
|
|
expect(header).toEqual([...STOCK_EXPORT_COLUMNS]);
|
|
});
|
|
|
|
it("writes each data row with the right cell types (booleans → TRUE/FALSE strings, numbers stay numeric)", () => {
|
|
const { sheets } = parseWorkbook(
|
|
buildStockExportWorkbook({
|
|
rows: [
|
|
{
|
|
...baseRows[0]!,
|
|
is_active: false,
|
|
},
|
|
],
|
|
generatedAt: "2026-09-02T12:00:00.000Z",
|
|
supplierNames: ["ACME"],
|
|
}),
|
|
);
|
|
const stock = sheets.Stock!;
|
|
const data = XLSX.utils.sheet_to_json<Record<string, unknown>>(stock);
|
|
expect(data).toHaveLength(1);
|
|
expect(data[0]?.is_active).toBe("FALSE");
|
|
expect(data[0]?.current_qty).toBe(10);
|
|
expect(data[0]?.sku).toBe("WID-1");
|
|
});
|
|
|
|
it("includes a Help sheet that lists every column with a description", () => {
|
|
const { sheets } = parseWorkbook(
|
|
buildStockExportWorkbook({
|
|
rows: baseRows,
|
|
generatedAt: "2026-09-02T12:00:00.000Z",
|
|
supplierNames: ["ACME"],
|
|
}),
|
|
);
|
|
const help = sheets.Help;
|
|
expect(help).toBeDefined();
|
|
const helpData = XLSX.utils.sheet_to_json<{ Column: string; "What it is": string }>(help, {
|
|
header: ["Column", "What it is"],
|
|
});
|
|
// First row is the header itself; subsequent rows are the columns.
|
|
const columns = helpData.slice(1).map((r) => r.Column);
|
|
for (const col of STOCK_EXPORT_COLUMNS) {
|
|
expect(columns).toContain(col);
|
|
}
|
|
});
|
|
|
|
it("handles the zero-supplierNames case (no crash)", () => {
|
|
const bytes = buildStockExportWorkbook({
|
|
rows: baseRows,
|
|
generatedAt: "2026-09-02T12:00:00.000Z",
|
|
supplierNames: [],
|
|
});
|
|
const { sheets } = parseWorkbook(bytes);
|
|
const data = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheets.Stock!);
|
|
expect(data).toHaveLength(baseRows.length);
|
|
});
|
|
});
|