/** * Tier 3: Combinatorial Cross-Feature Integration Tests * Minimum 15 end-to-end multi-module workflow integration test cases. */ import { describe, test, it, expect } from "./runner"; import { ProcessedReceipt, ReceiptData } from "../../src/lib/schema/receipt"; import { validateReceiptMath } from "../../src/lib/ai/mathValidator"; import { generateDeterministicDemoExtraction } from "../../src/lib/ai/extractor"; import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator"; import { generateAccountingCsv } from "../../src/lib/export/csvGenerator"; import { processReceiptImage } from "../../src/lib/image/processor"; import { FREE_SCAN_LIMIT } from "../../src/lib/limits"; import ExcelJS from "exceljs"; describe("Tier 3: Combinatorial Workflows & Cross-Feature Interactions", () => { // Workflow 1: MicroPrompt TANKEN_KFZ -> Ingestion -> Math Check -> Excel Generator test("Workflow 1: MicroPrompt (TANKEN_KFZ) -> AI extraction -> Math validation -> Dual-Sheet Excel export", async () => { // 1. Ingest Aral Tankstelle demo fixture const rawExtraction = generateDeterministicDemoExtraction("01_aral_tankbeleg_muenchen.jpg"); expect(rawExtraction.suggestedCategory).toBe("Tanken & KFZ"); // 2. Run deterministic math verification const mathResult = validateReceiptMath(rawExtraction); expect(mathResult.isMathValid).toBe(true); expect(mathResult.needsUserReview).toBe(false); // 3. Construct processed receipt record const receipt: ProcessedReceipt = { ...rawExtraction, id: "wf-1-aral", imageHash: "hash-aral-wf1", originalFileName: "01_aral_tankbeleg_muenchen.jpg", fileSizeBytes: 184000, createdAt: "2026-08-15T08:42:00.000Z", updatedAt: "2026-08-15T08:42:00.000Z", status: "ready", }; // 4. Generate Dual-Sheet Excel Workbook const excelBuffer = await generateDualSheetExcel([receipt]); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(excelBuffer as any); // 5. Verify Sheet 1 and Sheet 2 structure const s1 = workbook.getWorksheet("Belegübersicht"); const s2 = workbook.getWorksheet("Einzelpositionen Detail"); expect(s1).toBeDefined(); expect(s2).toBeDefined(); expect(s1!.getRow(2).getCell(3).value).toBe("Aral Tankstelle Station"); expect(s1!.getRow(2).getCell(10).value).toBe(68.45); // Verify SUM formula in summary row const sumGross = s1!.getRow(3).getCell(10); expect((sumGross.value as any)?.formula).toBe("SUM(J2:J2)"); }); // Workflow 2: MicroPrompt BEWIRTUNG -> AI Tip Extraction -> Accounting CSV Generation test("Workflow 2: MicroPrompt (BEWIRTUNG) -> AI extract with Tip -> Math check -> accounting CSV export", () => { // 1. Ingest Trattoria Bella Vista Bewirtungsbeleg fixture const rawExtraction = generateDeterministicDemoExtraction("02_trattoria_bewirtungsbeleg_berlin.jpg"); expect(rawExtraction.documentType).toBe("BEWIRTUNGSBELEG"); expect(rawExtraction.suggestedCategory).toBe("Bewirtung"); // 2. Validate line items contain tip item and food/drinks const tipItem = rawExtraction.lineItems.find((i) => i.description.includes("Trinkgeld")); expect(tipItem).toBeDefined(); expect(tipItem?.price).toBe(12.5); // 3. Check math validation const mathResult = validateReceiptMath(rawExtraction); expect(mathResult.isMathValid).toBe(true); // 4. Generate accounting CSV const receipt: ProcessedReceipt = { ...rawExtraction, id: "wf-2-trattoria", imageHash: "hash-trattoria-wf2", originalFileName: "02_trattoria.jpg", fileSizeBytes: 210000, createdAt: "2026-08-15T20:15:00.000Z", updatedAt: "2026-08-15T20:15:00.000Z", status: "ready", }; const accountingCsv = generateAccountingCsv([receipt]); // 5. Verify CSV formatting rules expect(accountingCsv.startsWith("\uFEFF")).toBe(true); expect(accountingCsv).toContain('"Trattoria Bella Vista - Bewirtung"'); expect(accountingCsv).toContain('"84,50"'); expect(accountingCsv).toContain('"70,97"'); expect(accountingCsv).toContain('"3,08"'); // 7% VAT expect(accountingCsv).toContain('"10,45"'); // 19% VAT }); // Workflow 3: MicroPrompt MWST_SPLIT -> Mixed Supermarket Receipt -> Dual-Sheet Excel test("Workflow 3: MicroPrompt (MWST_SPLIT) -> Multi-tax receipt -> Math check pass -> Dual-Sheet Excel", async () => { // 1. Ingest REWE supermarket receipt fixture const rawExtraction = generateDeterministicDemoExtraction("04_rewe_supermarkt_kassenbon.jpg"); expect(rawExtraction.taxBreakdown).toHaveLength(2); // 7% + 19% // 2. Verify math consistency const mathResult = validateReceiptMath(rawExtraction); expect(mathResult.isMathValid).toBe(true); const receipt: ProcessedReceipt = { ...rawExtraction, id: "wf-3-rewe", imageHash: "hash-rewe-wf3", originalFileName: "04_rewe.jpg", fileSizeBytes: 165000, createdAt: "2026-08-15T17:45:00.000Z", updatedAt: "2026-08-15T17:45:00.000Z", status: "ready", }; // 3. Export to Excel const buf = await generateDualSheetExcel([receipt]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.getRow(2).getCell(8).value).toBe(1.31); expect(s1?.getRow(2).getCell(9).value).toBe(0.76); expect(s1?.getRow(2).getCell(10).value).toBe(24.8); const s2 = wb.getWorksheet("Einzelpositionen Detail"); expect(s2?.rowCount).toBe(5); // 1 header + 4 items }); // Workflow 4: Multi-File Ingestion Batch -> Category Filtering -> Batch Export test("Workflow 4: Multi-file batch -> Preprocessing hashes -> Category filter -> Batch export", async () => { const names = [ "01_aral_tankbeleg_muenchen.jpg", "02_trattoria_bewirtungsbeleg_berlin.jpg", "04_rewe_supermarkt_kassenbon.jpg", ]; const processedBatch: ProcessedReceipt[] = names.map((name, i) => { const ext = generateDeterministicDemoExtraction(name); return { ...ext, id: `wf4-${i}`, imageHash: `hash-wf4-${name}`, originalFileName: name, fileSizeBytes: 100000 + i * 50000, createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready", }; }); expect(processedBatch).toHaveLength(3); // Filter by 'Tanken & KFZ' const tankenOnly = processedBatch.filter((r) => r.suggestedCategory === "Tanken & KFZ"); expect(tankenOnly).toHaveLength(1); expect(tankenOnly[0].merchant.name).toContain("Aral"); // Export full batch to Excel const buf = await generateDualSheetExcel(processedBatch); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); expect(s1?.rowCount).toBe(5); // 1 header + 3 data rows + 1 total }); // Workflow 5: Guest Quota Lifecycle (0 -> 14 -> 15 limit reached -> Pro upgrade -> Unlimited) test("Workflow 5: Guest scan quota lifecycle (0 -> 15 limit -> Pro switch -> unlimited)", () => { let scanCount = 0; let isPro = false; // Scan 1..14 for (let i = 0; i < 14; i++) { expect(isPro || scanCount < FREE_SCAN_LIMIT).toBe(true); scanCount++; } // Scan 15 (15th free scan allowed) expect(isPro || scanCount < FREE_SCAN_LIMIT).toBe(true); scanCount++; // Scan 16 attempt (blocked for guest) expect(isPro || scanCount < FREE_SCAN_LIMIT).toBe(false); // User purchases Pro license isPro = true; // Scan 16 attempt now succeeds expect(isPro || scanCount < FREE_SCAN_LIMIT).toBe(true); scanCount++; expect(scanCount).toBe(16); }); // Workflow 6: Discrepant Receipt -> MicroPrompt Resolution -> Status Update -> Export test("Workflow 6: Discrepant receipt -> 1-Click MicroPrompt confirmation -> Status Valid -> Export", async () => { // 1. Create a receipt with a discrepancy const rawData: ProcessedReceipt = { ...generateDeterministicDemoExtraction("01_aral_tankbeleg_muenchen.jpg"), id: "wf-6-disc", imageHash: "hash-wf6", originalFileName: "faded_receipt.jpg", fileSizeBytes: 120000, createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", netAmount: 40.0, // 40 + 10.93 != 68.45 validation: { isMathValid: false, isDuplicateSuspected: false, needsUserReview: true, reviewField: "taxBreakdown", reviewReason: "Netto + MwSt weicht von Brutto ab", }, status: "needs_review", }; expect(rawData.status).toBe("needs_review"); expect(rawData.validation.needsUserReview).toBe(true); // 2. User clicks "Confirm" in MicroPromptBar const resolvedReceipt: ProcessedReceipt = { ...rawData, netAmount: 57.52, // Fixed net validation: { ...rawData.validation, isMathValid: true, needsUserReview: false, reviewReason: null, }, status: "ready", }; expect(resolvedReceipt.status).toBe("ready"); expect(resolvedReceipt.validation.isMathValid).toBe(true); // 3. Export verified receipt const csv = generateAccountingCsv([resolvedReceipt]); expect(csv).toContain('"Valide"'); expect(csv).not.toContain('"Prüfung erforderlich"'); }); // Workflow 7: LiveTable Inline Editing -> Math Engine Recalculation -> Export Synchronization test("Workflow 7: LiveTable inline edit -> Math validation re-evaluation -> Export sync", () => { const initial = generateDeterministicDemoExtraction("01_aral_tankbeleg_muenchen.jpg"); // Inline edit: user corrects gross amount from 68.45 to 68.50 const editedData: Partial = { ...initial, totalAmount: { value: 68.5, confidence: 1.0 }, }; const revalidation = validateReceiptMath(editedData); // 57.52 + 10.93 = 68.45 != 68.50 (diff 0.05 > 0.03) -> flagged expect(revalidation.isMathValid).toBe(false); expect(revalidation.needsUserReview).toBe(true); // User also corrects net amount to 57.57 -> 57.57 + 10.93 = 68.50 const correctedData: Partial = { ...editedData, netAmount: 57.57, }; const correctedValidation = validateReceiptMath(correctedData); expect(correctedValidation.isMathValid).toBe(true); }); // Workflow 8: CMD+K Spotlight Search -> Filter & Modal Inspection Selection test("Workflow 8: CMD+K Spotlight search -> Select matching receipt -> Data binding matches", () => { const list = [ createMockReceipt({ id: "1", merchant: { name: "Aral", address: null, taxId: null, confidence: 1 } }), createMockReceipt({ id: "2", merchant: { name: "MediaMarkt", address: null, taxId: null, confidence: 1 } }), createMockReceipt({ id: "3", merchant: { name: "REWE", address: null, taxId: null, confidence: 1 } }), ]; const searchQuery = "mediamarkt"; const matched = list.filter((r) => r.merchant.name.toLowerCase().includes(searchQuery)); expect(matched).toHaveLength(1); expect(matched[0].id).toBe("2"); }); // Workflow 9: International Currency Receipt -> Pipeline Execution -> CSV & Excel test("Workflow 9: International currency receipt (USD) -> Validation -> Excel & CSV generation", async () => { const usdReceipt: ProcessedReceipt = { id: "wf-9-usd", merchant: { name: "Apple Store New York", address: "5th Ave", taxId: null, confidence: 0.99 }, date: { isoDate: "2026-08-10", time: "11:00", confidence: 0.95 }, documentType: "RECHNUNG", receiptNumber: "INV-US-9912", currency: "USD", totalAmount: { value: 108.87, confidence: 0.99 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 8.875, taxAmount: 8.87, netAmount: 100.0 }], lineItems: [{ description: "Magic Mouse 3", quantity: 1, price: 108.87, taxRate: 8.875 }], suggestedCategory: "Bürobedarf & IT", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-usd-wf9", originalFileName: "apple_ny.jpg", fileSizeBytes: 140000, createdAt: "2026-08-10T11:00:00.000Z", updatedAt: "2026-08-10T11:00:00.000Z", status: "ready", }; const val = validateReceiptMath(usdReceipt); expect(val.isMathValid).toBe(true); const csv = generateAccountingCsv([usdReceipt]); expect(csv).toContain('"Apple Store New York"'); expect(csv).toContain('"108,87"'); const buf = await generateDualSheetExcel([usdReceipt]); expect(buf.length).toBeGreaterThan(1000); }); // Workflow 10: Multi-Receipt Batch Export with Excel Dynamic SUM Spanning All Rows test("Workflow 10: 10-Receipt batch -> Dual-Sheet Excel with total formula =SUM(G2:G11)", async () => { const batch = Array.from({ length: 10 }, (_, i) => { const ext = generateDeterministicDemoExtraction(); return { ...ext, id: `batch-${i}`, imageHash: `hash-batch-${i}`, originalFileName: `receipt_${i}.jpg`, fileSizeBytes: 120000, createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready" as const, }; }); const buf = await generateDualSheetExcel(batch); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s1 = wb.getWorksheet("Belegübersicht"); const summaryRow = s1!.getRow(12); // 1 header + 10 rows + 1 summary const netSum = summaryRow.getCell(7); const grossSum = summaryRow.getCell(10); expect((netSum.value as any)?.formula).toBe("SUM(G2:G11)"); expect((grossSum.value as any)?.formula).toBe("SUM(J2:J11)"); }); // Workflow 11: Image Preprocessing Hash -> Duplicate Ingestion Alert test("Workflow 11: Image Preprocessing -> Hash computation -> Duplicate flag detection", async () => { // Valid 1x1 PNG (real magic bytes) — identical bytes, identical hashes. const png1x1 = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const p1 = await processReceiptImage(png1x1, "image/png"); const p2 = await processReceiptImage(png1x1, "image/png"); expect(p1.sha256Hash).toBe(p2.sha256Hash); // Simulate existing store check const existingHashes = new Set([p1.sha256Hash]); const isDuplicate = existingHashes.has(p2.sha256Hash); expect(isDuplicate).toBe(true); }); // Workflow 12: Tax-Exempt Invoices (0% VAT) -> Excel & CSV Export test("Workflow 12: Tax-exempt invoice -> Math check 0% tax -> Excel & CSV export", async () => { const taxExemptReceipt: ProcessedReceipt = { id: "wf-12-exempt", merchant: { name: "Deutsche Post AG", address: "Bonn", taxId: null, confidence: 0.98 }, date: { isoDate: "2026-08-14", time: "09:10", confidence: 0.95 }, documentType: "KASSENBON", receiptNumber: "DP-88712", currency: "EUR", totalAmount: { value: 35.0, confidence: 0.98 }, netAmount: 35.0, taxBreakdown: [{ ratePercent: 0, taxAmount: 0.0, netAmount: 35.0 }], lineItems: [{ description: "Briefmarken Set 50x", quantity: 1, price: 35.0, taxRate: 0 }], suggestedCategory: "Bürobedarf & IT", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-post-wf12", originalFileName: "post.jpg", fileSizeBytes: 110000, createdAt: "2026-08-14T09:10:00.000Z", updatedAt: "2026-08-14T09:10:00.000Z", status: "ready", }; const val = validateReceiptMath(taxExemptReceipt); expect(val.isMathValid).toBe(true); expect(val.calculatedTaxSum).toBe(0.0); const csv = generateAccountingCsv([taxExemptReceipt]); expect(csv).toContain('"35,00"'); const buf = await generateDualSheetExcel([taxExemptReceipt]); expect(buf.length).toBeGreaterThan(1000); }); // Workflow 13: Discount Voucher + Pfand Return Combination test("Workflow 13: Discounted receipt with Pfand return -> Line item cross-sum -> Excel detail", async () => { const discountReceipt: ProcessedReceipt = { id: "wf-13-disc", merchant: { name: "EDEKA Center", address: "München", taxId: "DE881122", confidence: 0.96 }, date: { isoDate: "2026-08-15", time: "16:00", confidence: 0.95 }, documentType: "KASSENBON", receiptNumber: "ED-9912", currency: "EUR", totalAmount: { value: 27.5, confidence: 0.98 }, netAmount: 24.3, taxBreakdown: [ { ratePercent: 7, taxAmount: 1.4, netAmount: 20.0 }, { ratePercent: 19, taxAmount: 1.8, netAmount: 4.3 }, ], lineItems: [ { description: "Einkauf Lebensmittel", quantity: 1, price: 21.4, taxRate: 7 }, { description: "Haushaltswaren", quantity: 1, price: 11.1, taxRate: 19 }, { description: "Treuerabatt Coupon", quantity: 1, price: -5.0, taxRate: 19 }, { description: "Pfandrückgabe", quantity: 1, price: -0.0, taxRate: 0 }, ], suggestedCategory: "Material & Einkauf", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-edeka-wf13", originalFileName: "edeka.jpg", fileSizeBytes: 140000, createdAt: "2026-08-15T16:00:00.000Z", updatedAt: "2026-08-15T16:00:00.000Z", status: "ready", }; const val = validateReceiptMath(discountReceipt); expect(val.calculatedItemsSum).toBe(27.5); expect(val.isMathValid).toBe(true); const buf = await generateDualSheetExcel([discountReceipt]); const wb = new ExcelJS.Workbook(); await wb.xlsx.load(buf as any); const s2 = wb.getWorksheet("Einzelpositionen Detail"); expect(s2?.rowCount).toBe(5); // 1 header + 4 line items }); // Workflow 14: Category Reassignment -> Category column in CSV test("Workflow 14: Category reassignment -> updated category column in CSV", () => { const receipt = createMockReceipt({ suggestedCategory: "Bewirtung", merchant: { name: "Restaurant Bella", address: null, taxId: null, confidence: 1 }, }); const csvInitial = generateAccountingCsv([receipt]); expect(csvInitial).toContain('"Restaurant Bella - Bewirtung"'); // Reassign category to 'Reisekosten & Hotel' const reassigned: ProcessedReceipt = { ...receipt, suggestedCategory: "Reisekosten & Hotel", }; const csvUpdated = generateAccountingCsv([reassigned]); expect(csvUpdated).toContain('"Restaurant Bella - Reisekosten & Hotel"'); }); // Workflow 15: Full End-to-End Lifecycle Execution test("Workflow 15: Full End-to-End Lifecycle: Ingest -> Preprocess -> Extract -> Validate -> Persist -> Dual Export", async () => { // 1. Ingest raw image buffer (valid 1x1 PNG — the whitelist rejects // non-image bytes, so the lifecycle starts from a real image) const mockImageBytes = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const preprocessed = await processReceiptImage(mockImageBytes, "image/png"); expect(preprocessed.sha256Hash).toBeDefined(); // 2. Multimodal AI Extraction (Deterministic generator) const extraction = generateDeterministicDemoExtraction("01_aral_tankbeleg_muenchen.jpg"); expect(extraction.merchant.name).toContain("Aral"); // 3. Math Determinism Verification const mathValidation = validateReceiptMath(extraction); expect(mathValidation.isMathValid).toBe(true); // 4. Create full processed receipt record const processedRecord: ProcessedReceipt = { ...extraction, id: `lifecycle_${Date.now()}`, imageHash: preprocessed.sha256Hash, originalFileName: "aral_tankstelle.jpg", fileSizeBytes: preprocessed.processedSizeBytes, previewUrl: preprocessed.base64DataUrl, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: "ready", }; // 5. Generate Dual-Sheet Excel const excelBuffer = await generateDualSheetExcel([processedRecord]); expect(excelBuffer.length).toBeGreaterThan(1000); // 6. Generate accounting CSV const csvContent = generateAccountingCsv([processedRecord]); expect(csvContent.startsWith("\uFEFF")).toBe(true); expect(csvContent).toContain("Aral Tankstelle Station"); }); }); function createMockReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: "mock-wf-id", merchant: { name: "Mock Vendor", address: null, taxId: null, confidence: 1 }, date: { isoDate: "2026-08-15", time: null, confidence: 1 }, documentType: "KASSENBON", receiptNumber: "MOCK-1", currency: "EUR", totalAmount: { value: 10.0, confidence: 1 }, netAmount: 8.4, taxBreakdown: [{ ratePercent: 19, taxAmount: 1.6, netAmount: 8.4 }], lineItems: [{ description: "Item", quantity: 1, price: 10.0, taxRate: 19 }], suggestedCategory: "Sonstiges", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-mock", originalFileName: "mock.jpg", fileSizeBytes: 1000, createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready", ...overrides, }; }