/** * Milestone 3 (R3) Challenger 2 Deep Adversarial Stress Suite * Empirical Verification of Requirement R3: * 1. Inline Cell Editing & Mathematical Recalculations (0 €, negative, corrupted strings, rapid sequential edits) * 2. Bulk Export Generation (Selected only vs all, CSV injection sanitization, UTF-8 BOM, dual-sheet XLSX integrity) * 3. Bulk Status Updates & Categorization & Deletion * 4. Selection & Filtering Edge Cases & Invariants */ import { describe, test, it, expect, beforeEach } from "./runner"; import { ProcessedReceipt, ReceiptCategory, PaymentMethod, DocumentType } from "../../src/lib/schema/receipt"; import { resolveReceiptStatusTier, getStatusTierMeta, ReceiptStatusTier, } from "../../src/components/dashboard/StatusBadge"; import { recalculateReceipt, confirmReceiptReviewed, receiptNeedsAttention, } from "../../src/lib/ai/recalculate"; import { parseAmountInput, formatAmountInput, formatMoney, grossOf, netOf, totalTaxOf, taxAmountForRate, collectTaxRates, } from "../../src/components/dashboard/receiptFormat"; import { generateDualSheetExcel } from "../../src/lib/export/excelGenerator"; import { generateAccountingCsv } from "../../src/lib/export/csvGenerator"; function createTestReceipt(overrides: Partial = {}): ProcessedReceipt { return { id: "challenger-rcpt-001", imageHash: "hash-ch-123", originalFileName: "sample_receipt.pdf", fileSizeBytes: 150000, previewUrl: "blob:http://localhost/sample.pdf", createdAt: "2026-08-15T12:00:00.000Z", updatedAt: "2026-08-15T12:00:00.000Z", status: "ready", documentType: "KASSENBON", receiptNumber: "RE-9901", currency: "EUR", merchant: { name: "Café Extrablatt GmbH", address: "Alexanderplatz 1, 10178 Berlin", taxId: "DE123456789", confidence: 0.95, }, date: { isoDate: "2026-08-15", time: "14:30", confidence: 0.95, }, totalAmount: { value: 119.0, confidence: 0.95, }, netAmount: 100.0, taxBreakdown: [ { ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }, ], lineItems: [ { description: "Espresso Doppio", quantity: 2, unitPrice: 3.5, price: 7.0, taxRate: 19, }, { description: "Frühstücksbuffet", quantity: 4, unitPrice: 28.0, price: 112.0, taxRate: 19, }, ], suggestedCategory: "Bewirtung", paymentMethod: "EC_KARTE", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, userConfirmed: false, reviewField: "none", reviewReason: null, issues: [], }, ...overrides, }; } describe("CHALLENGER-M3: Inline Cell Editing & Numeric Parsing Stress", () => { test("CH-M3.1: parseAmountInput parses varied valid and tricky numeric inputs", () => { expect(parseAmountInput("119,00")).toBe(119.0); expect(parseAmountInput("119.00")).toBe(119.0); expect(parseAmountInput("1.234,56 €")).toBe(1234.56); expect(parseAmountInput("1,234.56 EUR")).toBe(1234.56); expect(parseAmountInput("0,00")).toBe(0.0); expect(parseAmountInput("0")).toBe(0.0); expect(parseAmountInput("0.05")).toBe(0.05); expect(parseAmountInput("-50,00")).toBe(-50.0); expect(parseAmountInput("-12.34")).toBe(-12.34); expect(parseAmountInput(" 99,99 ")).toBe(99.99); }); test("CH-M3.2: parseAmountInput returns null for corrupted strings without crashing or returning NaN", () => { const corrupted = [ "abc", "", " ", "NaN", "Infinity", "-Infinity", "€€€", "EUR", "foo-bar-123", "12.34.56.78", ",,,", "... ", "$$$123", "[object Object]", "undefined", "null", ]; for (const val of corrupted) { const parsed = parseAmountInput(val); if (parsed !== null) { expect(Number.isFinite(parsed)).toBe(true); } } }); test("CH-M3.3: Editing Gross Amount to 0.00 € recalculates Net and Tax to 0.00 without division by zero", () => { const rcpt = createTestReceipt({ totalAmount: { value: 119.0, confidence: 0.9 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }], }); const updated = { ...rcpt, totalAmount: { ...rcpt.totalAmount, value: 0.0 }, }; const result = recalculateReceipt(updated, { editedField: "totalAmount" }); expect(result.totalAmount.value).toBe(0.0); expect(result.netAmount).toBe(0.0); expect(result.taxBreakdown[0].taxAmount).toBe(0.0); expect(result.taxBreakdown[0].netAmount).toBe(0.0); expect(Number.isNaN(result.netAmount ?? 0)).toBe(false); expect(Number.isFinite(result.netAmount ?? 0)).toBe(true); expect(result.validation.isMathValid).toBe(true); }); test("CH-M3.4: Editing Gross Amount to negative value (refund / credit note) distributes proportionally", () => { const rcpt = createTestReceipt({ totalAmount: { value: 119.0, confidence: 0.9 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }], lineItems: [], // No positive line items }); const updated = { ...rcpt, totalAmount: { ...rcpt.totalAmount, value: -119.0 }, }; const result = recalculateReceipt(updated, { editedField: "totalAmount" }); expect(result.totalAmount.value).toBe(-119.0); expect(result.netAmount).toBe(-100.0); expect(result.taxBreakdown[0].taxAmount).toBe(-19.0); expect(result.taxBreakdown[0].netAmount).toBe(-100.0); // Net + tax sum precisely equals negative gross (-100 + -19 = -119) expect((result.netAmount ?? 0) + (result.taxBreakdown[0]?.taxAmount ?? 0)).toBe(-119.0); }); test("CH-M3.5: Multi-tax rate gross recalculation with 19%, 7%, and 0% taxes", () => { const multiTax = createTestReceipt({ totalAmount: { value: 126.0, confidence: 0.9 }, netAmount: 110.0, taxBreakdown: [ { ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }, { ratePercent: 7, taxAmount: 0.7, netAmount: 10.0 }, { ratePercent: 0, taxAmount: 0.0, netAmount: 0.0 }, ], }); const updated = { ...multiTax, totalAmount: { ...multiTax.totalAmount, value: 252.0 }, }; const result = recalculateReceipt(updated, { editedField: "totalAmount" }); expect(result.totalAmount.value).toBe(252.0); // Gross doubled: 126 -> 252. Net and tax should double accordingly const sumTaxes = totalTaxOf(result); const net = netOf(result); expect(Math.round((net + sumTaxes) * 100) / 100).toBe(252.0); expect(result.validation.isMathValid).toBe(true); }); test("CH-M3.6: Editing Net Amount holds Gross constant and re-derives taxes", () => { const rcpt = createTestReceipt({ totalAmount: { value: 119.0, confidence: 0.9 }, netAmount: 100.0, taxBreakdown: [{ ratePercent: 19, taxAmount: 19.0, netAmount: 100.0 }], }); const updated = { ...rcpt, netAmount: 90.0, }; const result = recalculateReceipt(updated, { editedField: "netAmount" }); expect(result.totalAmount.value).toBe(119.0); expect(result.netAmount).toBe(90.0); expect(result.taxBreakdown[0].taxAmount).toBe(29.0); // 119 - 90 = 29 expect(result.taxBreakdown[0].netAmount).toBe(90.0); }); test("CH-M3.7: Rapid sequential edits across multiple fields preserve integrity", () => { let rcpt = createTestReceipt(); // 1. Edit Merchant rcpt = recalculateReceipt( { ...rcpt, merchant: { ...rcpt.merchant, name: "Neue Gastronomie Berlin" }, editedFields: { merchant: true }, }, { editedField: "merchant" } ); expect(rcpt.merchant.name).toBe("Neue Gastronomie Berlin"); expect(rcpt.merchant.confidence).toBe(1.0); // 2. Edit Date rcpt = recalculateReceipt( { ...rcpt, date: { ...rcpt.date, isoDate: "2026-08-01" }, editedFields: { ...rcpt.editedFields, date: true }, }, { editedField: "date" } ); expect(rcpt.date.isoDate).toBe("2026-08-01"); expect(rcpt.date.confidence).toBe(1.0); // 3. Edit Gross rcpt = recalculateReceipt( { ...rcpt, totalAmount: { ...rcpt.totalAmount, value: 595.0 }, editedFields: { ...rcpt.editedFields, totalAmount: true }, }, { editedField: "totalAmount" } ); expect(rcpt.totalAmount.value).toBe(595.0); expect(rcpt.netAmount).toBe(500.0); expect(rcpt.taxBreakdown[0].taxAmount).toBe(95.0); expect(rcpt.totalAmount.confidence).toBe(1.0); // 4. Edit Category rcpt = recalculateReceipt( { ...rcpt, suggestedCategory: "Reisekosten & Hotel", editedFields: { ...rcpt.editedFields, category: true }, }, { editedField: "category" } ); expect(rcpt.suggestedCategory).toBe("Reisekosten & Hotel"); expect(rcpt.validation.isMathValid).toBe(true); }); }); describe("CHALLENGER-M3: Bulk Export Generation & Security Sanitization", () => { const dataset: ProcessedReceipt[] = [ createTestReceipt({ id: "exp-1", merchant: { name: 'Firma "Test & Co." GmbH', address: "München", taxId: "DE1", confidence: 1.0 }, receiptNumber: "INV-001", totalAmount: { value: 119.0, confidence: 1.0 }, suggestedCategory: "Bürobedarf & IT", }), createTestReceipt({ id: "exp-2", merchant: { name: "=1+1; -- Formula Injection Test", address: "Frankfurt", taxId: "DE2", confidence: 1.0 }, receiptNumber: "INV-002", totalAmount: { value: 53.5, confidence: 1.0 }, netAmount: 50.0, taxBreakdown: [{ ratePercent: 7, taxAmount: 3.5, netAmount: 50.0 }], suggestedCategory: "Bewirtung", hospitality: { occasion: "Kundengespräch & Akquise", participants: "Max Mustermann, Erika Musterfrau" }, }), createTestReceipt({ id: "exp-3", merchant: { name: "@SUM(A1:A100) \n Multiline \r\n Carriage", address: "Hamburg", taxId: "DE3", confidence: 1.0 }, receiptNumber: "INV-003", totalAmount: { value: 200.0, confidence: 1.0 }, suggestedCategory: "Tanken & KFZ", }), ]; test("CH-M3.8: Bulk export for SELECTED items only excludes unselected records", async () => { const selectedIds = ["exp-1", "exp-3"]; const selectedReceipts = dataset.filter((r) => selectedIds.includes(r.id)); expect(selectedReceipts).toHaveLength(2); expect(selectedReceipts.some((r) => r.id === "exp-2")).toBe(false); // CSV const csv = generateAccountingCsv(selectedReceipts); expect(csv).toContain("INV-001"); expect(csv).toContain("INV-003"); expect(csv.includes("INV-002")).toBe(false); // Excel const xlsxBuffer = await generateDualSheetExcel(selectedReceipts); expect(xlsxBuffer).toBeDefined(); expect(xlsxBuffer.length).toBeGreaterThan(2000); }); test("CH-M3.9: Bulk export with EMPTY list handles cleanly without throwing", async () => { const emptyCsv = generateAccountingCsv([]); expect(emptyCsv).toBeDefined(); expect(emptyCsv.charCodeAt(0)).toBe(0xfeff); // UTF-8 BOM expect(emptyCsv).toContain("Laufende Nr"); const emptyXlsx = await generateDualSheetExcel([]); expect(emptyXlsx).toBeDefined(); expect(emptyXlsx.length).toBeGreaterThan(500); }); test("CH-M3.10: Accounting CSV properly quotes and escapes semicolons, quotes, and newlines", () => { const csv = generateAccountingCsv(dataset); // Must start with UTF-8 BOM expect(csv.charCodeAt(0)).toBe(0xfeff); // Double quotes must be escaped as "" expect(csv).toContain('""Test & Co.""'); // Formula-injection payloads (=, +, - @, tab, CR) are neutralized with a // leading apostrophe (OWASP CSV-injection mitigation) before quoting, so // the cell is exported as safe text, never as an evaluable formula expect(csv).toContain("'=1+1; -- Formula Injection Test\""); // Hospitality details must be exported when present expect(csv).toContain("Kundengespräch & Akquise"); expect(csv).toContain("Max Mustermann, Erika Musterfrau"); }); test("CH-M3.11: Dual-Sheet Excel generates both Belegübersicht and Einzelpositionen Detail sheets", async () => { const buffer = await generateDualSheetExcel(dataset); expect(buffer).toBeInstanceOf(Buffer); expect(buffer.length).toBeGreaterThan(5000); // Verify it's a valid ZIP / XLSX header (PK\x03\x04) expect(buffer[0]).toBe(0x50); // 'P' expect(buffer[1]).toBe(0x4b); // 'K' expect(buffer[2]).toBe(0x03); expect(buffer[3]).toBe(0x04); }); }); describe("CHALLENGER-M3: Bulk Status Updates & Batch Operations", () => { const mockBatch = [ createTestReceipt({ id: "b1", status: "needs_review", validation: { isMathValid: false, isDuplicateSuspected: false, needsUserReview: true, userConfirmed: false, reviewField: "taxBreakdown", reviewReason: "Diff", issues: [{ field: "taxBreakdown", severity: "error", message: "Diff" }] } }), createTestReceipt({ id: "b2", status: "ready", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, userConfirmed: false, reviewField: "none", reviewReason: null, issues: [] } }), createTestReceipt({ id: "b3", status: "ready", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, userConfirmed: false, reviewField: "none", reviewReason: null, issues: [] } }), ]; test("CH-M3.12: Bulk Status Update to 'confirmed' verifies all selected items and clears review flags", () => { const selectedIds = ["b1", "b2"]; const updatedBatch = mockBatch.map((r) => selectedIds.includes(r.id) ? confirmReceiptReviewed(r) : r ); const b1Updated = updatedBatch.find((r) => r.id === "b1"); const b2Updated = updatedBatch.find((r) => r.id === "b2"); const b3Updated = updatedBatch.find((r) => r.id === "b3"); expect(b1Updated?.validation.userConfirmed).toBe(true); expect(b1Updated?.validation.needsUserReview).toBe(false); expect(resolveReceiptStatusTier(b1Updated!)).toBe("confirmed"); expect(b2Updated?.validation.userConfirmed).toBe(true); expect(resolveReceiptStatusTier(b2Updated!)).toBe("confirmed"); expect(b3Updated?.validation.userConfirmed).toBe(false); expect(resolveReceiptStatusTier(b3Updated!)).toBe("scanned"); }); test("CH-M3.13: Bulk Categorize updates category and marks editedFields", () => { const selectedIds = ["b1", "b3"]; const targetCategory: ReceiptCategory = "Tanken & KFZ"; const updatedBatch = mockBatch.map((r) => { if (selectedIds.includes(r.id)) { const withCat = { ...r, suggestedCategory: targetCategory, editedFields: { ...(r.editedFields || {}), category: true }, }; return recalculateReceipt(withCat, { editedField: "category" }); } return r; }); expect(updatedBatch.find((r) => r.id === "b1")?.suggestedCategory).toBe("Tanken & KFZ"); expect(updatedBatch.find((r) => r.id === "b2")?.suggestedCategory).toBe("Bewirtung"); expect(updatedBatch.find((r) => r.id === "b3")?.suggestedCategory).toBe("Tanken & KFZ"); }); }); describe("CHALLENGER-M3: Filter & Selection Mathematical Invariants", () => { const testList: ProcessedReceipt[] = [ createTestReceipt({ id: "t1", date: { isoDate: "2026-08-15", time: "10:00", confidence: 1.0 }, totalAmount: { value: 25.0, confidence: 1.0 }, suggestedCategory: "Bewirtung" }), createTestReceipt({ id: "t2", date: { isoDate: "2026-08-14", time: "12:00", confidence: 1.0 }, totalAmount: { value: 75.0, confidence: 1.0 }, suggestedCategory: "Tanken & KFZ", validation: { isMathValid: false, isDuplicateSuspected: false, needsUserReview: true, userConfirmed: false, reviewField: "taxBreakdown", reviewReason: "Diff", issues: [] } }), createTestReceipt({ id: "t3", date: { isoDate: "2026-07-01", time: "09:00", confidence: 1.0 }, totalAmount: { value: 350.0, confidence: 1.0 }, suggestedCategory: "Bürobedarf & IT", validation: { isMathValid: true, isDuplicateSuspected: false, needsUserReview: false, userConfirmed: true, reviewField: "none", reviewReason: null, issues: [] } }), createTestReceipt({ id: "t4", date: { isoDate: "2026-01-10", time: "15:00", confidence: 1.0 }, totalAmount: { value: 12.0, confidence: 1.0 }, suggestedCategory: "Sonstiges" }), ]; test("CH-M3.14: Status count partition invariant holds (all === scanned + pending + confirmed)", () => { let scanned = 0; let pending = 0; let confirmed = 0; for (const r of testList) { const tier = resolveReceiptStatusTier(r); if (tier === "pending_review") pending++; else if (tier === "confirmed") confirmed++; else scanned++; } expect(scanned + pending + confirmed).toBe(testList.length); expect(scanned).toBe(2); expect(pending).toBe(1); expect(confirmed).toBe(1); }); test("CH-M3.15: Category collection and tax rate collection are deterministic sets", () => { const taxRates = collectTaxRates(testList); expect(Array.isArray(taxRates)).toBe(true); expect(taxRates).toContain(19); const categories = Array.from(new Set(testList.map((r) => r.suggestedCategory))); expect(categories).toHaveLength(4); expect(categories).toContain("Bewirtung"); expect(categories).toContain("Tanken & KFZ"); expect(categories).toContain("Bürobedarf & IT"); expect(categories).toContain("Sonstiges"); }); });