/** * Tier 1: Isolated Feature Coverage Test Suite * Minimum 75 test cases covering all 15 core features in isolation (>=5 tests per feature). */ import { describe, test, it, expect } from "./runner"; import tailwindConfig from "../../tailwind.config"; import { dictionaries } from "../../src/lib/i18n/dictionaries"; import { ReceiptExtractionSchema, ReceiptData, ProcessedReceipt, DocumentTypeSchema, ReceiptCategorySchema, } 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"; // Helper fixture generator function createMockReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: "test-rcpt-001", merchant: { name: "Aral Tankstelle", address: "Hauptstraße 42, München", taxId: "DE123456789", confidence: 0.98, }, date: { isoDate: "2026-08-15", time: "10:30", confidence: 0.95, }, documentType: "TANKBELEG", receiptNumber: "AR-998811", currency: "EUR", totalAmount: { value: 50.0, confidence: 0.99, }, netAmount: 42.02, taxBreakdown: [ { ratePercent: 19, taxAmount: 7.98, netAmount: 42.02, }, ], lineItems: [ { description: "Super E10 25.0l", quantity: 1, price: 50.0, taxRate: 19, }, ], suggestedCategory: "Tanken & KFZ", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, imageHash: "hash-001", originalFileName: "aral.jpg", fileSizeBytes: 124000, createdAt: "2026-08-15T10:30:00.000Z", updatedAt: "2026-08-15T10:30:00.000Z", status: "ready", ...overrides, }; } describe("Tier 1: Feature 1 — Zenith Silver Design System Tokens & 0px Check", () => { test("1.1 should define exact Zenith Silver color tokens in Tailwind config", () => { const extend = (tailwindConfig.theme as any)?.extend; const colors = extend?.colors; expect(colors.zenith).toBeDefined(); expect(colors.zenith.bg).toBe("#F6F9FF"); expect(colors.zenith.surface).toBe("#FFFFFF"); expect(colors.zenith.border).toBe("#E2E8F0"); expect(colors.zenith.text).toBe("#161C22"); expect(colors.zenith.black).toBe("#000000"); expect(colors.zenith.slate).toBe("#475569"); }); test("1.2 should define exact Typography font family tokens (Hanken, Inter, JetBrains Mono)", () => { const extend = (tailwindConfig.theme as any)?.extend; const fonts = extend?.fontFamily; expect(fonts.display[1]).toBe("Hanken Grotesk"); expect(fonts.sans[1]).toBe("Inter"); expect(fonts.mono[1]).toBe("JetBrains Mono"); }); test("1.3 should define architectural letter spacing tokens", () => { const extend = (tailwindConfig.theme as any)?.extend; const letterSpacing = extend?.letterSpacing; expect(letterSpacing.tightest).toBe("-0.04em"); expect(letterSpacing.tighter).toBe("-0.02em"); expect(letterSpacing.caps).toBe("0.1em"); }); test("1.4 should define laser scanline animation and keyframes", () => { const extend = (tailwindConfig.theme as any)?.extend; const animations = extend?.animation; const keyframes = extend?.keyframes; expect(animations["scan-line"]).toContain("scanline"); expect(keyframes.scanline).toBeDefined(); expect(animations["fade-in"]).toContain("fadeIn"); }); test("1.5 should verify surface container tokens in design system palette", () => { const extend = (tailwindConfig.theme as any)?.extend; const colors = extend?.colors.zenith; expect(colors.low).toBe("#EEF4FC"); expect(colors.container).toBe("#E8EEF6"); expect(colors.high).toBe("#E3E9F1"); expect(colors.highest).toBe("#DDE3EB"); }); test("1.6 should verify emerald and amber validation status tokens", () => { const extend = (tailwindConfig.theme as any)?.extend; const colors = extend?.colors; expect(colors.emerald["600"]).toBe("#059669"); expect(colors.amber["500"]).toBe("#f59e0b"); }); }); describe("Tier 1: Feature 2 — 7-Part Landing Page Architecture", () => { test("2.1 should define all German and English dictionaries for landing sections", () => { expect(dictionaries.de).toBeDefined(); expect(dictionaries.en).toBeDefined(); expect(dictionaries.de.hero).toBeDefined(); expect(dictionaries.de.features).toBeDefined(); expect(dictionaries.de.comparison).toBeDefined(); expect(dictionaries.de.pricing).toBeDefined(); expect(dictionaries.de.faq).toBeDefined(); expect(dictionaries.de.footer).toBeDefined(); }); test("2.2 should verify Hero value proposition and high-converting copy in German", () => { expect(dictionaries.de.hero.headlineStart).toBe("Kassenbon fotografieren."); expect(dictionaries.de.hero.headlineHighlight).toBe("Excel ist fertig."); expect(dictionaries.de.hero.badge).toContain("2026"); expect(dictionaries.de.hero.dropTitle).toContain("Belege hier ablegen"); }); test("2.3 should verify Hero value proposition in English", () => { expect(dictionaries.en.hero.headlineStart).toBe("Snap any receipt."); expect(dictionaries.en.hero.headlineHighlight).toBe("Excel is ready."); expect(dictionaries.en.hero.dropTitle).toContain("Drop receipts here"); }); test("2.4 should contain 6 detailed feature descriptions in German dictionary", () => { expect(dictionaries.de.features.f1_title).toContain("Dual-Sheet Excel"); expect(dictionaries.de.features.f2_title).toContain("Plausibilitäts-Check"); expect(dictionaries.de.features.f3_title).toContain("1-Klick Micro-Prompting"); expect(dictionaries.de.features.f4_title).toContain("Buchhaltungs-CSV"); // Storage is server-side PostgreSQL (see src/lib/storage/server.ts), not a // local-first IndexedDB cache, so the copy correctly promises cloud storage. expect(dictionaries.de.features.f5_title).toContain("Cloud-Speicherung"); expect(dictionaries.de.features.f6_title).toContain("Thermopapier"); }); test("2.5 should verify technical FAQ describes file contents without claiming tax treatment", () => { expect(dictionaries.de.faq.q1).toContain("verblassten Kassenbons"); // a2 must describe what the export actually contains, not assert legal/tax validity. expect(dictionaries.de.faq.a2).toContain("Einzelpositionen"); expect(dictionaries.de.faq.a2).toContain("Steuersatz"); expect(dictionaries.de.faq.a2).not.toContain("Pflichtangaben"); // Receipts live in PostgreSQL under the signed-in account, not in a local // IndexedDB cache — a3 must describe that account-scoped storage, not IndexedDB. expect(dictionaries.de.faq.a3).toContain("Konto"); expect(dictionaries.de.faq.a3).not.toContain("IndexedDB"); }); test("2.6 should verify footer legal and privacy links", () => { expect(dictionaries.de.footer.privacy).toBe("Datenschutz"); expect(dictionaries.de.footer.terms).toBe("AGB"); // No Impressum page or DSGVO-specific legal content exists on the site yet // (tracked separately), so the dictionary must not assert either claim. expect(dictionaries.de.footer.security).toBe("Sicherheit"); }); }); describe("Tier 1: Feature 3 — Hero-Scanner Drag-Drop & Camera Trigger", () => { test("3.1 should support multi-format MIME types for receipt ingestion", () => { const supportedTypes = [ "image/jpeg", "image/png", "image/webp", "image/heic", "application/pdf", ]; supportedTypes.forEach((type) => { expect(type).toMatch(/^(image\/|application\/pdf)/); }); }); test("3.2 should process raw buffer with Sharp processor into standardized JPEG data URL", async () => { // Generate a minimal valid 1x1 PNG buffer to test the pipeline const pngBuffer = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const result = await processReceiptImage(pngBuffer, "image/png"); expect(result.sha256Hash).toBeDefined(); expect(result.sha256Hash).toHaveLength(64); expect(result.base64DataUrl).toBeTruthy(); expect(result.base64DataUrl!).toContain("data:image/jpeg;base64,"); expect(result.mimeType).toBe("image/jpeg"); expect(result.storedMimeType).toBe("image/avif"); expect(result.previewUrl).toContain("data:image/avif;base64,"); expect(result.previewUrl.length).toBeLessThan(result.base64DataUrl!.length); expect(result.width).toBeGreaterThan(0); expect(result.height).toBeGreaterThan(0); }); test("3.3 should compute deterministic SHA-256 hash across identical inputs", async () => { // Valid 1x1 PNG (real magic bytes) — the whitelist rejects non-image buffers. const png = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); const res1 = await processReceiptImage(png, "image/png"); const res2 = await processReceiptImage(png, "image/png"); expect(res1.sha256Hash).toBe(res2.sha256Hash); }); test("3.4 should compute distinct SHA-256 hashes for different receipt images", async () => { const png = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ); // Valid 1x1 GIF — a different image than the PNG above. const gif = Buffer.from( "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", "base64" ); const res1 = await processReceiptImage(png, "image/png"); const res2 = await processReceiptImage(gif, "image/gif"); expect(res1.sha256Hash).not.toBe(res2.sha256Hash); }); test("3.5 should reject non-image buffers instead of passing raw bytes through", async () => { // Raw-fallback removal: arbitrary bytes must never reach the AI pipeline. const corruptedBuffer = Buffer.from("not_a_real_image_data"); let rejected = false; try { await processReceiptImage(corruptedBuffer, "image/jpeg"); } catch { rejected = true; } expect(rejected).toBe(true); }); }); describe("Tier 1: Feature 4 — Trust Bar & Social Proof Metrics", () => { const socialMetrics = [ { label: "Accuracy Index", value: "99.8%", standard: "Multimodal" }, { label: "Scan Latency", value: "1.4s", standard: "< 1.8s" }, { label: "Excel Engine", value: "Dual-Sheet", standard: "=SUM()" }, { label: "Privacy Standard", value: "Local-First", standard: "IndexedDB" }, ]; test("4.1 should have 99.8% precision benchmark", () => { const acc = socialMetrics.find((m) => m.label === "Accuracy Index"); expect(acc?.value).toBe("99.8%"); }); test("4.2 should have <= 1.4s average scan latency", () => { const lat = socialMetrics.find((m) => m.label === "Scan Latency"); expect(parseFloat(lat?.value || "0")).toBeLessThanOrEqual(1.8); }); test("4.3 should specify Dual-Sheet workbook architecture", () => { const xl = socialMetrics.find((m) => m.label === "Excel Engine"); expect(xl?.value).toBe("Dual-Sheet"); }); test("4.4 should enforce Local-First privacy guarantee", () => { const priv = socialMetrics.find((m) => m.label === "Privacy Standard"); expect(priv?.value).toBe("Local-First"); }); test("4.5 should back the export-format claims in the trust bar with real CSV output", () => { // The trust bar promises a CSV that opens cleanly in a German Excel and an // .xlsx with real dates. No fiscal/compliance claim is made anywhere, so // this asserts the format facts we do state instead. const csv = generateAccountingCsv([ createMockReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 1 } }), ]); expect(csv.startsWith("\uFEFF")).toBe(true); // UTF-8 BOM expect(csv).toContain(";"); // semicolon delimiter expect(csv).toContain("\r\n"); // CRLF expect(csv).toContain("15.08.2026"); // German date rendering of the ISO value expect(csv).not.toMatch(/DATEV|SKR0|EXTF|Buchungsstapel/); }); }); describe("Tier 1: Feature 5 — 3-Column Feature Grid & Workflow Terminal", () => { const workflowPhases = [ { phase: "01", name: "Ingestion & Multimodal Parsing", target: "RAW INGESTION" }, { phase: "02", name: "Deterministic Cross-Verification", target: "MATH VALIDATION" }, { phase: "03", name: "1-Click Excel & CSV Export", target: "DUAL-SHEET EXPORT" }, ]; test("5.1 should have exactly 3 sequential pipeline phases in the workflow", () => { expect(workflowPhases).toHaveLength(3); expect(workflowPhases[0].phase).toBe("01"); expect(workflowPhases[1].phase).toBe("02"); expect(workflowPhases[2].phase).toBe("03"); }); test("5.2 Phase 01 should target Raw Ingestion and parsing", () => { expect(workflowPhases[0].target).toBe("RAW INGESTION"); }); test("5.3 Phase 02 should target Deterministic Math Validation", () => { expect(workflowPhases[1].target).toBe("MATH VALIDATION"); }); test("5.4 Phase 03 should target Dual-Sheet Excel & CSV Export", () => { expect(workflowPhases[2].target).toBe("DUAL-SHEET EXPORT"); }); test("5.5 should provide simulated terminal state verification data", () => { const demoAral = generateDeterministicDemoExtraction("01_aral_tankbeleg_muenchen.jpg"); expect(demoAral.merchant.name).toContain("Aral"); expect(demoAral.suggestedCategory).toBe("Tanken & KFZ"); expect(demoAral.validation.isMathValid).toBe(true); }); }); describe("Tier 1: Feature 6 — Comparison Matrix & Technical FAQ Accordion", () => { const comparisonData = [ { name: "Dual-Sheet Excel Export", us: true, others: false }, { name: "Live =SUM() Formulas", us: true, others: false }, { name: "Deterministic Math Cross-Check", us: true, others: false }, { name: "Itemized Line Items & Quantities", us: true, others: false }, { name: "Accounting CSV (semicolon, decimal commas)", us: true, others: false }, { name: "Local-First Privacy Architecture", us: true, others: false }, { name: "1-Click Instant Guest Access", us: true, others: false }, ]; test("6.1 should provide all 7 comparison items against legacy OCR apps", () => { expect(comparisonData).toHaveLength(7); }); test("6.2 should score 100% feature capability across all matrix criteria", () => { comparisonData.forEach((item) => { expect(item.us).toBe(true); }); }); test("6.3 should verify comparison matrix dictionary keys in German", () => { const comp = dictionaries.de.comparison; expect(comp.row1_us).toContain("✅"); expect(comp.row1_others).toContain("❌"); expect(comp.row2_us).toContain("✅"); expect(comp.row5_us).toContain("✅"); }); test("6.4 should verify 4 primary FAQ entries in both DE and EN", () => { expect(dictionaries.de.faq.q1).toBeDefined(); expect(dictionaries.de.faq.q2).toBeDefined(); expect(dictionaries.de.faq.q3).toBeDefined(); expect(dictionaries.de.faq.q4).toBeDefined(); expect(dictionaries.en.faq.q1).toBeDefined(); expect(dictionaries.en.faq.q2).toBeDefined(); }); test("6.5 should verify pricing tiers (Free, Weekly, Annual, Lifetime)", () => { const pr = dictionaries.de.pricing; expect(pr.freePrice).toBe("0 €"); expect(pr.weeklyPrice).toBe("4,99 €"); expect(pr.annualPrice).toBe("39,99 €"); expect(pr.lifetimePrice).toBe("59,99 €"); }); }); describe("Tier 1: Feature 7 — Multi-Page Dashboard Routes (/dashboard, /activity, /export, /settings)", () => { const receiptsFixture: ProcessedReceipt[] = [ createMockReceipt({ id: "r-1", totalAmount: { value: 100.0, confidence: 0.99 }, netAmount: 84.03, taxBreakdown: [{ ratePercent: 19, taxAmount: 15.97, netAmount: 84.03 }], }), createMockReceipt({ id: "r-2", totalAmount: { value: 20.0, confidence: 0.99 }, netAmount: 18.69, taxBreakdown: [{ ratePercent: 7, taxAmount: 1.31, netAmount: 18.69 }], suggestedCategory: "Verpflegungsmehraufwand", }), createMockReceipt({ id: "r-3", totalAmount: { value: 50.0, confidence: 0.6 }, netAmount: 42.02, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.98, netAmount: 42.02 }], validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: true, reviewField: "totalAmount", reviewReason: "Bruttobetrag mit geringer Sicherheit erkannt.", }, }), ]; test("7.1 /dashboard: should correctly compute aggregated KPI metrics", () => { const totalGross = receiptsFixture.reduce((acc, r) => acc + r.totalAmount.value, 0); const totalNet = receiptsFixture.reduce((acc, r) => acc + (r.netAmount || 0), 0); const totalVat19 = receiptsFixture.reduce((acc, r) => { const t = r.taxBreakdown.find((x) => x.ratePercent === 19); return acc + (t?.taxAmount || 0); }, 0); const totalVat7 = receiptsFixture.reduce((acc, r) => { const t = r.taxBreakdown.find((x) => x.ratePercent === 7); return acc + (t?.taxAmount || 0); }, 0); const pendingReviewCount = receiptsFixture.filter( (r) => r.validation.needsUserReview || !r.validation.isMathValid ).length; expect(totalGross).toBe(170.0); expect(totalNet).toBeCloseTo(144.74, 2); expect(totalVat19).toBeCloseTo(23.95, 2); expect(totalVat7).toBeCloseTo(1.31, 2); expect(pendingReviewCount).toBe(1); }); test("7.2 /activity: should filter receipts by category correctly", () => { const tanken = receiptsFixture.filter((r) => r.suggestedCategory === "Tanken & KFZ"); const verpflegung = receiptsFixture.filter( (r) => r.suggestedCategory === "Verpflegungsmehraufwand" ); expect(tanken).toHaveLength(2); expect(verpflegung).toHaveLength(1); }); test("7.3 /activity: should filter receipts by merchant name query", () => { const results = receiptsFixture.filter((r) => r.merchant.name.toLowerCase().includes("aral") ); expect(results).toHaveLength(3); }); test("7.4 /export: should filter receipts by targetScope (ALL vs VALIDATED)", () => { const all = receiptsFixture; const validatedOnly = receiptsFixture.filter( (r) => !r.validation.needsUserReview && r.validation.isMathValid ); expect(all).toHaveLength(3); expect(validatedOnly).toHaveLength(2); }); test("7.5 /settings: should support GPT-5.6-Luna model configuration", () => { const supportedModels = [ "openai/gpt-5.6-luna", "openai/gpt-5-mini", "openai/gpt-4o-mini", "openai/gpt-5.6-terra", ]; expect(supportedModels).toContain("openai/gpt-5.6-luna"); }); }); describe("Tier 1: Feature 8 — Persistent Sidebar & TopNav", () => { const navRoutes = [ { label: "OVERVIEW", href: "/dashboard" }, { label: "ACTIVITY", href: "/dashboard/activity" }, { label: "EXPORT", href: "/dashboard/export" }, { label: "ACCOUNT", href: "/dashboard/settings" }, ]; test("8.1 should define all 4 dashboard sidebar navigation routes", () => { expect(navRoutes).toHaveLength(4); expect(navRoutes[0].href).toBe("/dashboard"); expect(navRoutes[1].href).toBe("/dashboard/activity"); expect(navRoutes[2].href).toBe("/dashboard/export"); expect(navRoutes[3].href).toBe("/dashboard/settings"); }); test("8.2 should have system branding 'ZENITH' and 'SYSTEM V1.0.2'", () => { const brand = "ZENITH SYSTEM V1.0.2"; expect(brand).toContain("ZENITH"); expect(brand).toContain("V1.0.2"); }); test("8.3 TopNav should provide CMD+K keyboard shortcut placeholder", () => { const kbd = "⌘K"; expect(kbd).toBe("⌘K"); }); test("8.4 TopNav should provide nominal system status indicator", () => { const status = "SYSTEM STATUS: ONLINE / NOMINAL"; expect(status).toContain("ONLINE / NOMINAL"); }); test("8.5 TopNav should support bilingual switching (de/en)", () => { const validLanguages = ["de", "en"]; expect(validLanguages).toContain("de"); expect(validLanguages).toContain("en"); }); }); describe("Tier 1: Feature 9 — Global CMD+K Spotlight Search Dialog", () => { const searchDataset: ProcessedReceipt[] = [ createMockReceipt({ id: "rcpt-1", merchant: { name: "Aral Tankstelle München", address: null, taxId: null, confidence: 1 }, receiptNumber: "AR-1002", date: { isoDate: "2026-08-10", time: null, confidence: 1 }, }), createMockReceipt({ id: "rcpt-2", merchant: { name: "REWE City Berlin", address: null, taxId: null, confidence: 1 }, receiptNumber: "RW-9901", date: { isoDate: "2026-08-12", time: null, confidence: 1 }, }), createMockReceipt({ id: "rcpt-3", merchant: { name: "Trattoria Bella Vista", address: null, taxId: null, confidence: 1 }, receiptNumber: "TR-4401", date: { isoDate: "2026-08-15", time: null, confidence: 1 }, }), ]; function searchReceipts(query: string, items: ProcessedReceipt[]): ProcessedReceipt[] { const q = query.trim().toLowerCase(); if (!q) return items; return items.filter( (r) => r.merchant.name.toLowerCase().includes(q) || (r.receiptNumber && r.receiptNumber.toLowerCase().includes(q)) || r.date.isoDate.includes(q) || r.suggestedCategory.toLowerCase().includes(q) ); } test("9.1 should search and find receipt by merchant name", () => { const results = searchReceipts("bella", searchDataset); expect(results).toHaveLength(1); expect(results[0].merchant.name).toContain("Trattoria"); }); test("9.2 should search and find receipt by receipt number", () => { const results = searchReceipts("RW-9901", searchDataset); expect(results).toHaveLength(1); expect(results[0].merchant.name).toContain("REWE"); }); test("9.3 should search and find receipt by ISO date string", () => { const results = searchReceipts("2026-08-10", searchDataset); expect(results).toHaveLength(1); expect(results[0].id).toBe("rcpt-1"); }); test("9.4 should perform case-insensitive search queries", () => { const resultsLower = searchReceipts("aral", searchDataset); const resultsUpper = searchReceipts("ARAL", searchDataset); expect(resultsLower).toHaveLength(1); expect(resultsUpper).toHaveLength(1); expect(resultsLower[0].id).toBe(resultsUpper[0].id); }); test("9.5 should return empty array when query does not match any record", () => { const results = searchReceipts("NonExistentVendor999", searchDataset); expect(results).toHaveLength(0); }); }); describe("Tier 1: Feature 10 — 1-Click Guest Flow & Local Storage", () => { test("10.1 should define FREE_SCAN_LIMIT as exactly 15 scans", () => { expect(FREE_SCAN_LIMIT).toBe(15); }); test("10.2 should allow guest scanning within free quota (< 15)", () => { const currentScanCount = 14; const isPro = false; const canScan = isPro || currentScanCount < FREE_SCAN_LIMIT; expect(canScan).toBe(true); }); test("10.3 should block guest scanning and trigger paywall when scan limit reached (>= 15)", () => { const currentScanCount = 15; const isPro = false; const canScan = isPro || currentScanCount < FREE_SCAN_LIMIT; expect(canScan).toBe(false); }); test("10.4 should bypass scan limit when user has active Pro status", () => { const currentScanCount = 50; const isPro = true; const canScan = isPro || currentScanCount < FREE_SCAN_LIMIT; expect(canScan).toBe(true); }); test("10.5 should generate valid demo receipt fixtures for instant guest onboarding", () => { const demoNames = [ "01_aral_tankbeleg_muenchen.jpg", "02_trattoria_bewirtungsbeleg_berlin.jpg", "04_rewe_supermarkt_kassenbon.jpg", ]; demoNames.forEach((name) => { const extracted = generateDeterministicDemoExtraction(name); expect(extracted.merchant.name).toBeDefined(); expect(extracted.totalAmount.value).toBeGreaterThan(0); expect(extracted.validation.isMathValid).toBe(true); }); }); test("10.6 should synchronize 15-scan quota across German and English dictionaries", () => { expect(dictionaries.de.pricing.freeF1).toContain("15"); expect(dictionaries.en.pricing.freeF1).toContain("15"); expect(dictionaries.de.paywall.triggerReasons.scanLimit).toContain("15"); expect(dictionaries.en.paywall.triggerReasons.scanLimit).toContain("15"); expect(dictionaries.en.paywall.modalSubtitle).toContain("15"); }); test("10.7 should reject 16th scan when guest user attempts to process beyond FREE_SCAN_LIMIT", () => { const isPro = false; const batchSizes = [1, 5, 10, 15]; const allowed = batchSizes.map((count) => isPro || count <= FREE_SCAN_LIMIT); expect(allowed.every((val) => val === true)).toBe(true); const sixteenthScanAllowed = isPro || 16 <= FREE_SCAN_LIMIT; expect(sixteenthScanAllowed).toBe(false); }); }); describe("Tier 1: Feature 11 — Multimodal AI Extraction Schema & Fallback", () => { test("11.1 should validate valid German receipt JSON against Zod Extraction Schema", () => { const validData = { merchant: { name: "MediaMarkt Saturn", address: "Alexanderplatz 3, Berlin", taxId: "DE119876543", confidence: 0.95, }, date: { isoDate: "2026-08-15", time: "14:30", confidence: 0.92, }, documentType: "KASSENBON", receiptNumber: "MM-9902", currency: "EUR", totalAmount: { value: 129.99, confidence: 0.98, }, netAmount: 109.24, taxBreakdown: [ { ratePercent: 19, taxAmount: 20.75, netAmount: 109.24, }, ], lineItems: [ { description: "Logitech MX Master 3S", quantity: 1, price: 129.99, taxRate: 19, }, ], suggestedCategory: "Bürobedarf & IT", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null, }, }; const parseResult = ReceiptExtractionSchema.safeParse(validData); expect(parseResult.success).toBe(true); }); test("11.2 should fail validation when required merchant name or total is missing", () => { const invalidData = { merchant: { confidence: 0.9 }, // Missing name date: { isoDate: "2026-08-15", confidence: 0.9 }, }; const parseResult = ReceiptExtractionSchema.safeParse(invalidData); expect(parseResult.success).toBe(false); }); test("11.3 should validate DocumentType enum values", () => { const validTypes = [ "KASSENBON", "RECHNUNG", "TANKBELEG", "BEWIRTUNGSBELEG", "PARKTICKET", "SONSTIGES", ]; validTypes.forEach((t) => { const res = DocumentTypeSchema.safeParse(t); expect(res.success).toBe(true); }); const invalidTypeRes = DocumentTypeSchema.safeParse("INVALID_TYPE"); expect(invalidTypeRes.success).toBe(false); }); test("11.4 should validate ReceiptCategory enum values", () => { const validCategories = [ "Bewirtung", "Reisekosten & Hotel", "Tanken & KFZ", "Bürobedarf & IT", "Verpflegungsmehraufwand", "Material & Einkauf", "Sonstiges", ]; validCategories.forEach((cat) => { const res = ReceiptCategorySchema.safeParse(cat); expect(res.success).toBe(true); }); }); test("11.5 should generate deterministic fallback for unrecognized file name", () => { const fallback = generateDeterministicDemoExtraction("unknown_receipt.png"); expect(fallback.merchant.name).toBe("MediaMarkt Saturn Holding"); expect(fallback.suggestedCategory).toBe("Bürobedarf & IT"); expect(fallback.totalAmount.value).toBe(129.99); }); }); describe("Tier 1: Feature 12 — Math Determinism Engine (Netto + MwSt 7%/19% = Brutto)", () => { test("12.1 should validate exact single-tax receipt (19% VAT)", () => { const receipt: Partial = { totalAmount: { value: 119.0, confidence: 0.98 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }], lineItems: [{ description: "Office Chair", quantity: 1, price: 119.0, taxRate: 19 }], }; const res = validateReceiptMath(receipt); expect(res.isMathValid).toBe(true); expect(res.needsUserReview).toBe(false); expect(res.calculatedGross).toBe(119.0); expect(res.calculatedTaxSum).toBe(19.0); expect(res.calculatedNetSum).toBe(100.0); }); test("12.2 should validate mixed-tax receipt (7% food + 19% drinks/goods)", () => { const receipt: Partial = { totalAmount: { value: 24.8, confidence: 0.95 }, netAmount: 22.73, taxBreakdown: [ { ratePercent: 7, taxAmount: 1.31, netAmount: 18.75 }, { ratePercent: 19, taxAmount: 0.76, netAmount: 3.98 }, ], lineItems: [ { description: "Food", quantity: 1, price: 20.06, taxRate: 7 }, { description: "Napkins", quantity: 1, price: 4.74, taxRate: 19 }, ], }; const res = validateReceiptMath(receipt); expect(res.isMathValid).toBe(true); expect(res.needsUserReview).toBe(false); expect(res.calculatedTaxSum).toBe(2.07); }); test("12.3 should flag math discrepancy when Net + Tax deviates from Gross by > 0.03€", () => { const receipt: Partial = { totalAmount: { value: 100.0, confidence: 0.95 }, netAmount: 70.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 10.0, netAmount: 70.0 }], // 70 + 10 = 80 != 100 lineItems: [], }; const res = validateReceiptMath(receipt); expect(res.isMathValid).toBe(false); expect(res.needsUserReview).toBe(true); expect(res.reviewField).toBe("taxBreakdown"); expect(res.reviewReason).toContain("weicht von Brutto"); }); test("12.4 should flag low confidence on total amount (< 0.85)", () => { const receipt: Partial = { totalAmount: { value: 45.0, confidence: 0.72 }, netAmount: 37.82, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.18, netAmount: 37.82 }], lineItems: [{ description: "Book", quantity: 1, price: 45.0, taxRate: 19 }], }; const res = validateReceiptMath(receipt); expect(res.needsUserReview).toBe(true); expect(res.reviewField).toBe("totalAmount"); expect(res.reviewReason).toContain("geringer Sicherheit"); }); test("12.5 should flag low confidence on date (< 0.80)", () => { const receipt: Partial = { totalAmount: { value: 45.0, confidence: 0.95 }, netAmount: 37.82, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.18, netAmount: 37.82 }], date: { isoDate: "2026-08-15", time: null, confidence: 0.65 }, }; const res = validateReceiptMath(receipt); expect(res.needsUserReview).toBe(true); expect(res.reviewField).toBe("date"); expect(res.reviewReason).toContain("Belegdatum"); }); test("12.6 should flag low confidence on merchant name (< 0.75)", () => { const receipt: Partial = { totalAmount: { value: 45.0, confidence: 0.95 }, netAmount: 37.82, taxBreakdown: [{ ratePercent: 19, taxAmount: 7.18, netAmount: 37.82 }], merchant: { name: "Blurry Merchant", address: null, taxId: null, confidence: 0.6 }, }; const res = validateReceiptMath(receipt); expect(res.needsUserReview).toBe(true); expect(res.reviewField).toBe("merchant"); expect(res.reviewReason).toContain("Händlername"); }); }); describe("Tier 1: Feature 13 — 1-Click Micro-Prompt Bar & Directives", () => { test("13.1 should confirm and resolve review state upon 1-click confirmation", () => { const unconfirmed = createMockReceipt({ validation: { isMathValid: false, isDuplicateSuspected: false, needsUserReview: true, reviewField: "taxBreakdown", reviewReason: "Steuerprüfung nötig", }, status: "needs_review", }); // Simulate 1-click confirmation logic const confirmed: ProcessedReceipt = { ...unconfirmed, validation: { ...unconfirmed.validation, needsUserReview: false, isMathValid: true, reviewReason: null, }, status: "ready", }; expect(confirmed.validation.needsUserReview).toBe(false); expect(confirmed.validation.isMathValid).toBe(true); expect(confirmed.status).toBe("ready"); }); test("13.2 should define Bewirtung micro-tag prompt requirements (§4 Abs. 5 EStG)", () => { const bewirtungDirective = "Besonderer Bewirtungsbeleg: Extrahiere explizit Trinkgeld (Tip), Anlass der Bewirtung, Teilnehmer und trenne Speisen von Getränken."; expect(bewirtungDirective).toContain("Trinkgeld"); expect(bewirtungDirective).toContain("Teilnehmer"); }); test("13.3 should define Tanken & KFZ micro-tag prompt requirements", () => { const fuelDirective = "Tankbeleg: Extrahiere Kraftstoffart (Diesel, Super E10), getankte Literanzahl und Literpreis."; expect(fuelDirective).toContain("Kraftstoffart"); expect(fuelDirective).toContain("Literanzahl"); }); test("13.4 should define MwSt-Split micro-tag prompt requirements", () => { const splitDirective = "Mehrwertsteuer-Split: Strikte Zuordnung 7% ermäßigt vs 19% Regelsteuersatz für jede Position."; expect(splitDirective).toContain("7%"); expect(splitDirective).toContain("19%"); }); test("13.5 should filter only receipts needing review for the MicroPromptBar", () => { const list: ProcessedReceipt[] = [ createMockReceipt({ id: "1", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, reviewField: "none", reviewReason: null } }), createMockReceipt({ id: "2", validation: { isMathValid: false, isDuplicateSuspected: false, needsUserReview: true, reviewField: "taxBreakdown", reviewReason: "Diff" } }), ]; const needingReview = list.filter((r) => r.validation.needsUserReview || !r.validation.isMathValid); expect(needingReview).toHaveLength(1); expect(needingReview[0].id).toBe("2"); }); }); describe("Tier 1: Feature 14 — Dual-Sheet Excel (.xlsx) Generation with =SUM() Formulas", () => { test("14.1 should generate a valid Excel binary buffer", async () => { const receipts = [createMockReceipt()]; const buffer = await generateDualSheetExcel(receipts); expect(buffer).toBeInstanceOf(Buffer); expect(buffer.length).toBeGreaterThan(1000); }); test("14.2 should produce exactly 2 worksheets: 'Belegübersicht' and 'Einzelpositionen Detail'", async () => { const receipts = [createMockReceipt()]; const buffer = await generateDualSheetExcel(receipts); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer as any); expect(workbook.worksheets).toHaveLength(2); expect(workbook.worksheets[0].name).toBe("Belegübersicht"); expect(workbook.worksheets[1].name).toBe("Einzelpositionen Detail"); }); test("14.3 Sheet 1 summary row should contain dynamic Excel SUM formulas", async () => { const receipts = [ createMockReceipt({ id: "r-1" }), createMockReceipt({ id: "r-2" }), ]; const buffer = await generateDualSheetExcel(receipts); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer as any); const sheet1 = workbook.getWorksheet("Belegübersicht"); expect(sheet1).toBeDefined(); // Summary row is at row receipts.length + 2 = 4 const summaryRow = sheet1!.getRow(4); const netCell = summaryRow.getCell(7); // Column G = netAmount const grossCell = summaryRow.getCell(10); // Column J = grossAmount expect((netCell.value as any)?.formula).toBe("SUM(G2:G3)"); expect((grossCell.value as any)?.formula).toBe("SUM(J2:J3)"); }); test("14.4 Sheet 2 should populate itemized line items with descriptions and quantities", async () => { const receipts = [ createMockReceipt({ lineItems: [ { description: "Item A", quantity: 2, price: 20.0, taxRate: 19 }, { description: "Item B", quantity: 1, price: 10.0, taxRate: 7 }, ], }), ]; const buffer = await generateDualSheetExcel(receipts); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer as any); const sheet2 = workbook.getWorksheet("Einzelpositionen Detail"); expect(sheet2).toBeDefined(); expect(sheet2!.rowCount).toBeGreaterThanOrEqual(3); // 1 header + 2 items }); test("14.5 should handle empty receipt list without crashing", async () => { const buffer = await generateDualSheetExcel([]); expect(buffer).toBeInstanceOf(Buffer); expect(buffer.length).toBeGreaterThan(500); }); test("14.6 should format currency numbers with German € pattern", async () => { const receipts = [createMockReceipt()]; const buffer = await generateDualSheetExcel(receipts); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer as any); const sheet1 = workbook.getWorksheet("Belegübersicht"); const row2 = sheet1!.getRow(2); expect(row2.getCell(10).numFmt).toContain("€"); }); }); describe("Tier 1: Feature 15 — Accounting CSV Export with UTF-8 BOM & German Decimals", () => { test("15.1 should prepend UTF-8 BOM (\\uFEFF) to the CSV output", () => { const receipts = [createMockReceipt()]; const csv = generateAccountingCsv(receipts); expect(csv.startsWith("\uFEFF")).toBe(true); }); test("15.2 should use semicolon (;) as column delimiter", () => { const receipts = [createMockReceipt()]; const csv = generateAccountingCsv(receipts); const headerLine = csv.replace("\uFEFF", "").split("\r\n")[0]; expect(headerLine).toContain(";"); expect(headerLine.split(";").length).toBeGreaterThanOrEqual(10); }); test("15.3 should format decimals with German comma (e.g. 50,00)", () => { const receipts = [createMockReceipt({ totalAmount: { value: 1234.56, confidence: 1 } })]; const csv = generateAccountingCsv(receipts); expect(csv).toContain("1234,56"); }); test("15.4 should format dates in German DD.MM.YYYY standard", () => { const receipts = [createMockReceipt({ date: { isoDate: "2026-08-15", time: null, confidence: 1 } })]; const csv = generateAccountingCsv(receipts); expect(csv).toContain("15.08.2026"); }); test("15.5 should escape double quotes inside text fields per RFC 4180", () => { const receipts = [ createMockReceipt({ merchant: { name: 'Bäcker "Kruste" GmbH', address: null, taxId: null, confidence: 1 }, }), ]; const csv = generateAccountingCsv(receipts); expect(csv).toContain('""Kruste""'); }); test("15.6 should join rows with CRLF (\\r\\n) line endings for Excel compatibility", () => { const receipts = [createMockReceipt({ id: "1" }), createMockReceipt({ id: "2" })]; const csv = generateAccountingCsv(receipts); expect(csv).toContain("\r\n"); const lines = csv.replace("\uFEFF", "").split("\r\n"); expect(lines.length).toBe(3); // 1 header + 2 rows }); });