/** * Tier 2: Boundary & Corner Cases Test Suite * Minimum 75 boundary, edge case, and stress tests (>=5 tests per feature area). */ import { describe, test, it, expect } from "./runner"; import { ReceiptExtractionSchema, ReceiptData, ProcessedReceipt, } from "../../src/lib/schema/receipt"; import { validateReceiptMath } from "../../src/lib/ai/mathValidator"; import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator"; import { generateAccountingCsv } from "../../src/lib/export/csvGenerator"; import { processReceiptImage } from "../../src/lib/image/processor"; import fs from "fs"; import path from "path"; import ExcelJS from "exceljs"; function createBoundaryReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: `b-rcpt-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`, merchant: { name: "Boundary Merchant", address: null, taxId: null, confidence: 1.0, }, date: { isoDate: "2026-08-15", time: null, confidence: 1.0, }, documentType: "KASSENBON", receiptNumber: "BOUND-001", currency: "EUR", totalAmount: { value: 10.0, confidence: 1.0, }, netAmount: 8.4, taxBreakdown: [ { ratePercent: 19, taxAmount: 1.6, netAmount: 8.4, }, ], lineItems: [ { description: "Test Item", quantity: 1, price: 10.0, taxRate: 19, }, ], suggestedCategory: "Sonstiges", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-bound-01", originalFileName: "boundary.jpg", fileSizeBytes: 1000, createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready", ...overrides, }; } describe("Tier 2: Boundary 1 — 0.00€ Zero-Value Receipts & Nil Tax Cases", () => { test("B1.1 should validate 0.00€ receipt with 0.00€ net and empty tax breakdown", () => { const r = createBoundaryReceipt({ totalAmount: { value: 0.0, confidence: 1.0 }, netAmount: 0.0, taxBreakdown: [{ ratePercent: 0, taxAmount: 0.0, netAmount: 0.0 }], lineItems: [{ description: "Free Promotion", quantity: 1, price: 0.0, taxRate: 0 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); expect(val.needsUserReview).toBe(false); expect(val.calculatedGross).toBe(0.0); }); test("B1.2 should format 0.00€ in the accounting CSV correctly as '0,00'", () => { const r = createBoundaryReceipt({ totalAmount: { value: 0.0, confidence: 1.0 }, netAmount: 0.0, taxBreakdown: [], }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"0,00"'); }); test("B1.3 should generate Excel workbook for 0.00€ receipt without division errors", async () => { const r = createBoundaryReceipt({ totalAmount: { value: 0.0, confidence: 1.0 }, netAmount: 0.0, taxBreakdown: [], lineItems: [], }); const buf = await generateDualSheetExcel([r]); expect(buf.length).toBeGreaterThan(500); }); test("B1.4 should handle 0-quantity line items", () => { const r = createBoundaryReceipt({ totalAmount: { value: 0.0, confidence: 1.0 }, netAmount: 0.0, lineItems: [{ description: "Zero Item", quantity: 0, price: 0.0, taxRate: 0 }], }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(0.0); }); test("B1.5 should accept 0.00€ values in Zod schema", () => { const data = createBoundaryReceipt({ totalAmount: { value: 0.0, confidence: 1.0 }, netAmount: 0.0, }); const res = ReceiptExtractionSchema.safeParse(data); expect(res.success).toBe(true); }); }); describe("Tier 2: Boundary 2 — Commercial Rounding Tolerances (0.01€, 0.02€, 0.03€ vs >0.03€)", () => { test("B2.1 should accept 0.01€ rounding delta (within 0.03€ epsilon)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 11.91, confidence: 1.0 }, netAmount: 10.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }], // 10.00 + 1.90 = 11.90 (diff 0.01) }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B2.2 should accept 0.02€ rounding delta (within 0.03€ epsilon)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 11.92, confidence: 1.0 }, netAmount: 10.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }], // diff 0.02 }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B2.3 should accept exactly 0.03€ boundary rounding delta", () => { const r = createBoundaryReceipt({ totalAmount: { value: 11.93, confidence: 1.0 }, netAmount: 10.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }], // diff 0.03 }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B2.4 should flag discrepancy when delta exceeds 0.03€ (e.g. 0.04€)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 11.94, confidence: 1.0 }, netAmount: 10.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.9, netAmount: 10.0 }], // diff 0.04 > 0.03 }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(false); expect(val.needsUserReview).toBe(true); }); test("B2.5 should handle fractional line item prices sum accumulating within epsilon", () => { // 3 items at 3.33€ = 9.99€ vs gross 10.00€ (diff 0.01€) const r = createBoundaryReceipt({ totalAmount: { value: 10.0, confidence: 1.0 }, netAmount: 8.4, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.6, netAmount: 8.4 }], lineItems: [ { description: "Item 1", quantity: 1, price: 3.33, taxRate: 19 }, { description: "Item 2", quantity: 1, price: 3.33, taxRate: 19 }, { description: "Item 3", quantity: 1, price: 3.33, taxRate: 19 }, ], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); }); describe("Tier 2: Boundary 3 — Multi-Tax Splits & Exotic Tax Rates", () => { test("B3.1 should validate 7% and 19% split accurately", () => { const r = createBoundaryReceipt({ totalAmount: { value: 126.0, confidence: 1.0 }, netAmount: 115.97, taxBreakdown: [ { ratePercent: 7, taxAmount: 7.0, netAmount: 100.0 }, // 107.00 { ratePercent: 19, taxAmount: 3.03, netAmount: 15.97 }, // 19.00 ], lineItems: [ { description: "Food item", quantity: 1, price: 107.0, taxRate: 7 }, { description: "Non-food item", quantity: 1, price: 19.0, taxRate: 19 }, ], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B3.2 should validate 0% tax-free receipts (e.g. postage, medical, sovereign fees)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 50.0, confidence: 1.0 }, netAmount: 50.0, taxBreakdown: [{ ratePercent: 0, taxAmount: 0.0, netAmount: 50.0 }], lineItems: [{ description: "Postage Stamps", quantity: 1, price: 50.0, taxRate: 0 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); expect(val.calculatedTaxSum).toBe(0.0); }); test("B3.3 should accept international VAT rate like Swiss 8.1%", () => { const r = createBoundaryReceipt({ currency: "CHF", totalAmount: { value: 108.1, confidence: 1.0 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 8.1, taxAmount: 8.1, netAmount: 100.0 }], lineItems: [{ description: "Swiss Hotel", quantity: 1, price: 108.1, taxRate: 8.1 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B3.4 should accept UK standard 20% VAT", () => { const r = createBoundaryReceipt({ currency: "GBP", totalAmount: { value: 120.0, confidence: 1.0 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 20, taxAmount: 20.0, netAmount: 100.0 }], lineItems: [{ description: "London Transport", quantity: 1, price: 120.0, taxRate: 20 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B3.5 should handle receipts with 4 separate tax baskets", () => { const r = createBoundaryReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: 88.0, taxBreakdown: [ { ratePercent: 19, taxAmount: 4.75, netAmount: 25.0 }, { ratePercent: 7, taxAmount: 1.75, netAmount: 25.0 }, { ratePercent: 5, taxAmount: 1.25, netAmount: 25.0 }, { ratePercent: 0, taxAmount: 0.0, netAmount: 13.0 }, ], lineItems: [], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(false); // 88 + 7.75 = 95.75 != 100 -> correctly flagged }); }); describe("Tier 2: Boundary 4 — Negative Line Items, Discounts & Bottle Deposits (Pfand)", () => { test("B4.1 should validate receipt with negative bottle deposit (Pfand -0.25€)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 9.75, confidence: 1.0 }, netAmount: 8.19, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.56, netAmount: 8.19 }], lineItems: [ { description: "Getränk Kiste", quantity: 1, price: 10.0, taxRate: 19 }, { description: "Leergut Rückgabe (Pfand)", quantity: 1, price: -0.25, taxRate: 19 }, ], }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(9.75); expect(val.isMathValid).toBe(true); }); test("B4.2 should validate receipt with commercial discount voucher (-10.00€)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 40.0, confidence: 1.0 }, netAmount: 33.61, taxBreakdown: [{ ratePercent: 19, taxAmount: 6.39, netAmount: 33.61 }], lineItems: [ { description: "Wareneinkauf", quantity: 1, price: 50.0, taxRate: 19 }, { description: "Aktionsgutschein Rabatt", quantity: 1, price: -10.0, taxRate: 19 }, ], }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(40.0); expect(val.isMathValid).toBe(true); }); test("B4.3 should export negative line items into Excel Sheet 2 without syntax errors", async () => { const r = createBoundaryReceipt({ lineItems: [ { description: "Item 1", quantity: 1, price: 20.0, taxRate: 19 }, { description: "Pfand", quantity: 1, price: -3.5, taxRate: 19 }, ], }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s2 = wb.getWorksheet("Einzelpositionen Detail"); expect(s2?.rowCount).toBeGreaterThanOrEqual(3); }); test("B4.4 should handle multiple Pfand return lines", () => { const r = createBoundaryReceipt({ totalAmount: { value: 15.0, confidence: 1.0 }, netAmount: 12.61, taxBreakdown: [{ ratePercent: 19, taxAmount: 2.39, netAmount: 12.61 }], lineItems: [ { description: "Mineralwasser 12x", quantity: 1, price: 21.6, taxRate: 19 }, { description: "Pfand Kiste", quantity: 1, price: -3.3, taxRate: 19 }, { description: "Pfand Flaschen", quantity: 1, price: -3.3, taxRate: 19 }, ], }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(15.0); }); test("B4.5 should format negative values in the accounting CSV properly", () => { const r = createBoundaryReceipt({ totalAmount: { value: -15.0, confidence: 1.0 }, netAmount: -12.61, taxBreakdown: [{ ratePercent: 19, taxAmount: -2.39, netAmount: -12.61 }], }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"-15,00"'); }); }); describe("Tier 2: Boundary 5 — Missing Net Amounts & Auto-Reconciliation", () => { test("B5.1 should handle null netAmount when single 19% tax breakdown exists", () => { const r = createBoundaryReceipt({ totalAmount: { value: 119.0, confidence: 1.0 }, netAmount: null, taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: null }], }); const val = validateReceiptMath(r); // When net is null, net check doesn't fail expect(val.isMathValid).toBe(true); }); test("B5.2 Excel export should compute fallback net (gross - taxes) when netAmount is null", async () => { const r = createBoundaryReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: null, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: null }], }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); const netCell = s1?.getRow(2).getCell(7); expect(netCell?.value).toBeCloseTo(84.03, 2); }); test("B5.3 Accounting CSV should compute fallback net when netAmount is null", () => { const r = createBoundaryReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: null, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: null }], }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"84,03"'); }); test("B5.4 should handle missing net on multi-tax receipt", () => { const r = createBoundaryReceipt({ totalAmount: { value: 100.0, confidence: 1.0 }, netAmount: null, taxBreakdown: [ { ratePercent: 7, taxAmount: 3.5, netAmount: null }, { ratePercent: 19, taxAmount: 9.5, netAmount: null }, ], }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"87,00"'); // 100 - (3.5 + 9.5) = 87.00 }); test("B5.5 should handle empty taxBreakdown with null netAmount", () => { const r = createBoundaryReceipt({ totalAmount: { value: 50.0, confidence: 1.0 }, netAmount: null, taxBreakdown: [], }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"50,00"'); }); }); describe("Tier 2: Boundary 6 — Foreign Currencies & Unicode Characters", () => { test("B6.1 should parse US Dollar (USD) currency code", () => { const r = createBoundaryReceipt({ currency: "USD", totalAmount: { value: 49.99, confidence: 1 } }); expect(r.currency).toBe("USD"); }); test("B6.2 should parse British Pound (GBP) currency code", () => { const r = createBoundaryReceipt({ currency: "GBP", totalAmount: { value: 25.5, confidence: 1 } }); expect(r.currency).toBe("GBP"); }); test("B6.3 should parse Swiss Franc (CHF) currency code", () => { const r = createBoundaryReceipt({ currency: "CHF", totalAmount: { value: 85.0, confidence: 1 } }); expect(r.currency).toBe("CHF"); }); test("B6.4 should preserve Japanese characters in merchant name", () => { const r = createBoundaryReceipt({ merchant: { name: "ファミリーマート (FamilyMart Tokyo)", address: null, taxId: null, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain("ファミリーマート"); }); test("B6.5 should preserve Cyrillic and Eastern European diacritics", () => { const r = createBoundaryReceipt({ merchant: { name: "Kraków Restauracja & Café Łódź", address: null, taxId: null, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain("Kraków Restauracja & Café Łódź"); }); }); describe("Tier 2: Boundary 7 — Extreme Transaction Amounts (0.01€ to 1,000,000.00€)", () => { test("B7.1 should handle minimal transaction: 0.01€ (1 Cent)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 0.01, confidence: 1 }, netAmount: 0.01, taxBreakdown: [{ ratePercent: 0, taxAmount: 0.0, netAmount: 0.01 }], lineItems: [{ description: "Plastic Bag", quantity: 1, price: 0.01, taxRate: 0 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); expect(val.calculatedGross).toBe(0.01); }); test("B7.2 should handle large enterprise purchase: 99,999.99€", () => { const r = createBoundaryReceipt({ totalAmount: { value: 99999.99, confidence: 1 }, netAmount: 84033.61, taxBreakdown: [{ ratePercent: 19, taxAmount: 15966.38, netAmount: 84033.61 }], lineItems: [{ description: "Enterprise Server Rack", quantity: 1, price: 99999.99, taxRate: 19 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B7.3 should handle 1,000,000.00€ transaction", () => { const r = createBoundaryReceipt({ totalAmount: { value: 1000000.0, confidence: 1 }, netAmount: 840336.13, taxBreakdown: [{ ratePercent: 19, taxAmount: 159663.87, netAmount: 840336.13 }], }); const val = validateReceiptMath(r); expect(val.isMathValid).toBe(true); }); test("B7.4 Accounting CSV should format 1234567.89 as '1234567,89'", () => { const r = createBoundaryReceipt({ totalAmount: { value: 1234567.89, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"1234567,89"'); }); test("B7.5 Excel export should write large numbers as numeric values without NaN", async () => { const r = createBoundaryReceipt({ totalAmount: { value: 500000.5, confidence: 1 }, netAmount: 420168.49, taxBreakdown: [{ ratePercent: 19, taxAmount: 79832.01, netAmount: 420168.49 }], }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.getRow(2).getCell(10).value).toBe(500000.5); }); }); describe("Tier 2: Boundary 8 — Strict 0px Border & Zenith Silver Component Style Audits", () => { const componentPaths = [ "src/components/landing/Header.tsx", "src/components/landing/HeroSection.tsx", "src/components/landing/SocialProofSection.tsx", "src/components/landing/FeaturesSection.tsx", "src/components/landing/ComparisonTable.tsx", "src/components/landing/FAQSection.tsx", "src/components/landing/Footer.tsx", "src/components/dashboard/Sidebar.tsx", "src/components/dashboard/TopNav.tsx", "src/components/dashboard/LiveTable.tsx", "src/components/dashboard/ExportBar.tsx", "src/app/(app)/dashboard/page.tsx", "src/app/(app)/dashboard/activity/page.tsx", "src/app/(app)/dashboard/export/page.tsx", "src/app/(app)/dashboard/settings/page.tsx", ]; test("B8.1 verify existence of all 15 audited Zenith Silver components & routes", () => { componentPaths.forEach((relPath) => { const fullPath = path.resolve(process.cwd(), relPath); expect(fs.existsSync(fullPath)).toBe(true); }); }); test("B8.2 audit components for absence of soft rounded cards (rounded-2xl, rounded-3xl)", () => { componentPaths.forEach((relPath) => { const fullPath = path.resolve(process.cwd(), relPath); const content = fs.readFileSync(fullPath, "utf-8"); // Check for rounded-2xl or rounded-3xl which are banned under Zenith Silver 0px design system expect(content).not.toMatch(/rounded-2xl/); expect(content).not.toMatch(/rounded-3xl/); }); }); test("B8.3 audit components for Zenith Silver border tokens (#E2E8F0)", () => { const sample = fs.readFileSync(path.resolve(process.cwd(), "src/components/dashboard/Sidebar.tsx"), "utf-8"); expect(sample).toContain("#E2E8F0"); }); test("B8.4 audit Landing Header for sticky 0px frame structure", () => { const header = fs.readFileSync(path.resolve(process.cwd(), "src/components/landing/Header.tsx"), "utf-8"); expect(header).toContain("sticky"); expect(header).toContain("border-b"); }); test("B8.5 audit LiveTable for sharp 0px borders", () => { const liveTable = fs.readFileSync(path.resolve(process.cwd(), "src/components/dashboard/LiveTable.tsx"), "utf-8"); expect(liveTable).toContain("border-collapse"); }); }); describe("Tier 2: Boundary 9 — Line Item Extremes (Empty List, 100+ Items)", () => { test("B9.1 should handle receipt with 0 line items without crashing", () => { const r = createBoundaryReceipt({ lineItems: [] }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(0); }); test("B9.2 should handle receipt with 100 line items", () => { const items = Array.from({ length: 100 }, (_, i) => ({ description: `Wholesale Item #${i + 1}`, quantity: 1, price: 2.5, taxRate: 19, })); const r = createBoundaryReceipt({ totalAmount: { value: 250.0, confidence: 1 }, netAmount: 210.08, taxBreakdown: [{ ratePercent: 19, taxAmount: 39.92, netAmount: 210.08 }], lineItems: items, }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(250.0); expect(val.isMathValid).toBe(true); }); test("B9.3 should populate all 100 line items into Excel Sheet 2", async () => { const items = Array.from({ length: 100 }, (_, i) => ({ description: `Wholesale Item #${i + 1}`, quantity: 1, price: 2.5, taxRate: 19, })); const r = createBoundaryReceipt({ lineItems: items }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s2 = wb.getWorksheet("Einzelpositionen Detail"); expect(s2?.rowCount).toBe(101); // 1 header + 100 items }); test("B9.4 should handle line items with large quantity (e.g. quantity 500)", () => { const r = createBoundaryReceipt({ totalAmount: { value: 1000.0, confidence: 1 }, lineItems: [{ description: "Screws Bulk", quantity: 500, price: 1000.0, taxRate: 19 }], }); const val = validateReceiptMath(r); expect(val.calculatedItemsSum).toBe(1000.0); }); test("B9.5 should handle line items with emoji and special punctuation in description", async () => { const r = createBoundaryReceipt({ lineItems: [ { description: "☕ Bio Espresso & 🥐 Croissant (2x) [Special!]", quantity: 1, price: 8.5, taxRate: 19 }, ], }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s2 = wb.getWorksheet("Einzelpositionen Detail"); expect(s2?.getRow(2).getCell(4).value).toContain("Bio Espresso"); }); }); describe("Tier 2: Boundary 10 — Date Format Variances & Leap Years", () => { test("B10.1 should format standard ISO date 2026-08-15 as 15.08.2026", () => { const r = createBoundaryReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 1 } }); const csv = generateAccountingCsv([r]); expect(csv).toContain("15.08.2026"); }); test("B10.2 should handle leap year date 2028-02-29", () => { const r = createBoundaryReceipt({ date: { isoDate: "2028-02-29", time: null, confidence: 1 } }); const csv = generateAccountingCsv([r]); expect(csv).toContain("29.02.2028"); }); test("B10.3 should handle year-end transition date 2026-12-31", () => { const r = createBoundaryReceipt({ date: { isoDate: "2026-12-31", time: "23:59", confidence: 1 } }); const csv = generateAccountingCsv([r]); expect(csv).toContain("31.12.2026"); }); test("B10.4 should flag review when date confidence < 0.80", () => { const r = createBoundaryReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 0.75 } }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(true); expect(val.reviewField).toBe("date"); }); test("B10.5 should not alter date format if already formatted non-ISO", () => { const r = createBoundaryReceipt({ date: { isoDate: "15.08.2026", time: null, confidence: 1 } }); const csv = generateAccountingCsv([r]); expect(csv).toContain("15.08.2026"); }); }); describe("Tier 2: Boundary 11 — Confidence Score Boundary Limits", () => { test("B11.1 Total confidence exactly 0.85 should NOT trigger review", () => { const r = createBoundaryReceipt({ totalAmount: { value: 50.0, confidence: 0.85 }, netAmount: 42.02, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.98, netAmount: 42.02 }], lineItems: [{ description: "Item", quantity: 1, price: 50.0, taxRate: 19 }], }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(false); }); test("B11.2 Total confidence 0.849 should trigger review on totalAmount", () => { const r = createBoundaryReceipt({ totalAmount: { value: 50.0, confidence: 0.849 }, netAmount: 42.02, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.98, netAmount: 42.02 }], lineItems: [{ description: "Item", quantity: 1, price: 50.0, taxRate: 19 }], }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(true); expect(val.reviewField).toBe("totalAmount"); }); test("B11.3 Date confidence exactly 0.80 should NOT trigger review", () => { const r = createBoundaryReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 0.8 } }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(false); }); test("B11.4 Date confidence 0.799 should trigger review on date", () => { const r = createBoundaryReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 0.799 } }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(true); expect(val.reviewField).toBe("date"); }); test("B11.5 Merchant confidence 0.749 should trigger review on merchant", () => { const r = createBoundaryReceipt({ merchant: { name: "Vendor", address: null, taxId: null, confidence: 0.749 }, }); const val = validateReceiptMath(r); expect(val.needsUserReview).toBe(true); expect(val.reviewField).toBe("merchant"); }); }); describe("Tier 2: Boundary 12 — Excel Formula Escaping & Malicious Payload Sanitization", () => { test("B12.1 should safely handle merchant names starting with formula injection characters (=, +, -, @)", async () => { const r = createBoundaryReceipt({ merchant: { name: "=CMD|' /C calc'!A0", address: null, taxId: null, confidence: 1 }, }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.getRow(2).getCell(3).value).toBe("=CMD|' /C calc'!A0"); }); test("B12.2 should handle long merchant names (300+ characters) without throwing", async () => { const longName = "A".repeat(300); const r = createBoundaryReceipt({ merchant: { name: longName, address: null, taxId: null, confidence: 1 }, }); const buf = await generateDualSheetExcel([r]); expect(buf.length).toBeGreaterThan(1000); }); test("B12.3 should preserve sheet names within 31 chars Excel limit", async () => { const buf = await generateDualSheetExcel([createBoundaryReceipt()]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); wb.worksheets.forEach((ws) => { expect(ws.name.length).toBeLessThanOrEqual(31); }); }); test("B12.4 should handle missing receiptNumber gracefully with '-' in Excel", async () => { const r = createBoundaryReceipt({ receiptNumber: null }); const buf = await generateDualSheetExcel([r]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.getRow(2).getCell(6).value).toBe("-"); }); test("B12.5 should handle batch of 50 receipts in Excel generation", async () => { const batch = Array.from({ length: 50 }, (_, i) => createBoundaryReceipt({ id: `b-50-${i}`, totalAmount: { value: i + 1, confidence: 1 } }) ); const buf = await generateDualSheetExcel(batch); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.rowCount).toBe(52); // 1 header + 50 rows + 1 summary }); }); describe("Tier 2: Boundary 13 — Accounting CSV Escaping & Complex Delimiters", () => { test("B13.1 should escape semicolons inside merchant name", () => { const r = createBoundaryReceipt({ merchant: { name: "München Tankstelle; Filiale 42", address: null, taxId: null, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"München Tankstelle; Filiale 42"'); }); test("B13.2 should escape nested double quotes in merchant name per RFC 4180", () => { const r = createBoundaryReceipt({ merchant: { name: 'Restaurant "Zur goldenen Gans"', address: null, taxId: null, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain('""Zur goldenen Gans""'); }); test("B13.3 should preserve German umlauts (Ä, Ö, Ü, ß) in CSV", () => { const r = createBoundaryReceipt({ merchant: { name: "Bäckerei Schönbrunn & Süßwaren", address: null, taxId: null, confidence: 1 }, }); const csv = generateAccountingCsv([r]); expect(csv).toContain("Bäckerei Schönbrunn & Süßwaren"); }); test("B13.4 should format empty receiptNumber as empty quoted string in CSV", () => { const r = createBoundaryReceipt({ receiptNumber: null }); const csv = generateAccountingCsv([r]); const row = csv.split("\r\n")[1]; expect(row).toContain('""'); }); test("B13.5 should correctly populate booking text (Buchungstext) column", () => { const r = createBoundaryReceipt({ merchant: { name: "Aral", address: null, taxId: null, confidence: 1 }, suggestedCategory: "Tanken & KFZ", }); const csv = generateAccountingCsv([r]); expect(csv).toContain('"Aral - Tanken & KFZ"'); }); }); describe("Tier 2: Boundary 14 — Image Preprocessing Boundaries (Tiny, Giant, Extreme Ratios)", () => { test("B14.1 should process minimal 1x1 image buffer cleanly", async () => { const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const res = await processReceiptImage(png1x1, "image/png"); expect(res.width).toBe(1); expect(res.height).toBe(1); expect(res.sha256Hash).toHaveLength(64); }); test("B14.2 should generate valid base64 data URL with JPEG prefix", async () => { const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const res = await processReceiptImage(png1x1, "image/png"); expect(res.base64DataUrl?.startsWith("data:image/jpeg;base64,")).toBe(true); }); test("B14.3 should reject uncompressed raw buffers instead of passing them through", async () => { // Raw-fallback removal: a buffer with no image signature must be refused. const raw = Buffer.alloc(500, 0xff); let rejected = false; try { await processReceiptImage(raw, "image/jpeg"); } catch { rejected = true; } expect(rejected).toBe(true); }); test("B14.4 should track original vs processed byte sizes", async () => { const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const res = await processReceiptImage(png1x1, "image/png"); expect(res.originalSizeBytes).toBe(png1x1.length); expect(res.processedSizeBytes).toBeGreaterThan(0); }); test("B14.5 should compute SHA-256 hash on input before any transform", async () => { // Valid 1x1 PNG (real magic bytes) — the hash is taken over the raw input // before any sharp transform. const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const res = await processReceiptImage(png1x1, "image/png"); const crypto = await import("crypto"); const expectedHash = crypto.createHash("sha256").update(png1x1).digest("hex"); expect(res.sha256Hash).toBe(expectedHash); }); }); describe("Tier 2: Boundary 15 — Duplicate Detection & UUID Collision Resistance", () => { test("B15.1 should detect identical SHA-256 hashes for re-uploaded duplicate receipts", async () => { // Valid 1x1 PNG — identical bytes must produce identical hashes. const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const resA = await processReceiptImage(png1x1, "image/png"); const resB = await processReceiptImage(png1x1, "image/png"); expect(resA.sha256Hash).toBe(resB.sha256Hash); }); test("B15.2 should produce unique random receipt IDs across 1,000 rapid iterations", () => { const ids = new Set(); for (let i = 0; i < 1000; i++) { const id = `rcpt_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; ids.add(id); } expect(ids.size).toBe(1000); }); test("B15.3 should set isDuplicateSuspected flag in schema", () => { const r = createBoundaryReceipt({ validation: { isMathValid: true, isDuplicateSuspected: true, needsUserReview: true, reviewField: "none", reviewReason: "Mögliches Duplikat erkannt", }, }); expect(r.validation.isDuplicateSuspected).toBe(true); }); test("B15.4 should maintain ID and createdAt upon status update", () => { const original = createBoundaryReceipt({ id: "fix-id-1", createdAt: "2026-08-15T10:00:00.000Z" }); const updated: ProcessedReceipt = { ...original, status: "needs_review", updatedAt: "2026-08-15T11:00:00.000Z", }; expect(updated.id).toBe(original.id); expect(updated.createdAt).toBe(original.createdAt); expect(updated.status).toBe("needs_review"); }); test("B15.5 should preserve raw text field if provided by OCR model", () => { const r = createBoundaryReceipt({ rawText: "ARAL TANKSTELLE\nSUPER E10 50,00 EUR" }); expect(r.rawText).toContain("ARAL TANKSTELLE"); }); });