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 } { 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>(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>(sheets.Stock!); expect(data).toHaveLength(baseRows.length); }); });