154 lines
4.5 KiB
TypeScript
154 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);
|
||
|
|
});
|
||
|
|
});
|