Files
scan-receipts/tests/e2e/tier2_boundaries.test.ts
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

878 lines
34 KiB
TypeScript

/**
* 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> = {}): 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<string>();
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");
});
});