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>
This commit is contained in:
653
tests/e2e/tier4_workloads.test.ts
Normal file
653
tests/e2e/tier4_workloads.test.ts
Normal file
@@ -0,0 +1,653 @@
|
||||
/**
|
||||
* Tier 4: Real-World German Receipt Workload Scenarios
|
||||
* Minimum 8 realistic German accounting & tax compliance workload scenarios.
|
||||
*/
|
||||
|
||||
import { describe, test, it, expect } from "./runner";
|
||||
import { 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 ExcelJS from "exceljs";
|
||||
|
||||
describe("Tier 4: Real-World German Receipt Workload Scenarios", () => {
|
||||
// Scenario 1: Aral Tankstelle Fuel Receipt with Liters & 19% VAT
|
||||
test("Scenario 1: Aral Tankstelle Fuel Receipt with Liters & 19% VAT", async () => {
|
||||
const aralReceipt: ProcessedReceipt = {
|
||||
id: "scen-1-aral",
|
||||
merchant: {
|
||||
name: "Aral Tankstelle Station München",
|
||||
address: "Landsberger Str. 402, 81241 München",
|
||||
taxId: "DE129482910",
|
||||
confidence: 0.99,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-14",
|
||||
time: "07:35",
|
||||
confidence: 0.98,
|
||||
},
|
||||
documentType: "TANKBELEG",
|
||||
receiptNumber: "AR-882910",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 78.45,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 65.92,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 12.53,
|
||||
netAmount: 65.92,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "Super E10 (42.50 l x 1.729 €/l)",
|
||||
quantity: 1,
|
||||
price: 73.48,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Kaffee Crema Groß 0.3l",
|
||||
quantity: 1,
|
||||
price: 3.5,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Croissant Natur",
|
||||
quantity: 1,
|
||||
price: 1.47,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Tanken & KFZ",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-aral-scen-1",
|
||||
originalFileName: "01_aral_tankbeleg_muenchen.jpg",
|
||||
fileSizeBytes: 245000,
|
||||
createdAt: "2026-08-14T07:35:00.000Z",
|
||||
updatedAt: "2026-08-14T07:35:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
// 1. Math check
|
||||
const val = validateReceiptMath(aralReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
expect(val.calculatedGross).toBe(78.45);
|
||||
expect(val.calculatedItemsSum).toBe(78.45);
|
||||
|
||||
// 2. Accounting CSV check
|
||||
const csv = generateAccountingCsv([aralReceipt]);
|
||||
expect(csv).toContain('"Aral Tankstelle Station München"');
|
||||
expect(csv).toContain('"78,45"');
|
||||
expect(csv).toContain('"12,53"');
|
||||
expect(csv).toContain('"14.08.2026"');
|
||||
|
||||
// 3. Excel check
|
||||
const buf = await generateDualSheetExcel([aralReceipt]);
|
||||
const wb = new ExcelJS.Workbook();
|
||||
await wb.xlsx.load(buf as any);
|
||||
const s1 = wb.getWorksheet("Belegübersicht");
|
||||
expect(s1?.getRow(2).getCell(4).value).toBe("Tanken & KFZ");
|
||||
});
|
||||
|
||||
// Scenario 2: REWE Supermarkt Mixed Food (7%) & Non-Food (19%) Basket
|
||||
test("Scenario 2: REWE Supermarkt Mixed Food (7%) & Non-Food (19%) Basket with Pfand", async () => {
|
||||
const reweReceipt: ProcessedReceipt = {
|
||||
id: "scen-2-rewe",
|
||||
merchant: {
|
||||
name: "REWE Markt GmbH Filiale 441",
|
||||
address: "Friedrichstraße 100, 10117 Berlin",
|
||||
taxId: "DE811122334",
|
||||
confidence: 0.98,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "18:20",
|
||||
confidence: 0.97,
|
||||
},
|
||||
documentType: "KASSENBON",
|
||||
receiptNumber: "RW-2026-8819",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 34.62,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 31.78,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 7,
|
||||
taxAmount: 1.63,
|
||||
netAmount: 23.32,
|
||||
},
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 1.21,
|
||||
netAmount: 8.46,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "REWE Bio Vollmilch 3.8% 1l",
|
||||
quantity: 2,
|
||||
price: 3.18,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Bio Bananen Fairtrade 1.2 kg",
|
||||
quantity: 1,
|
||||
price: 2.39,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Dinkel Sauerteigbrot 500g",
|
||||
quantity: 1,
|
||||
price: 3.99,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Gouda jung Bio 400g",
|
||||
quantity: 1,
|
||||
price: 4.49,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Espresso Bohnen Bio 1kg",
|
||||
quantity: 1,
|
||||
price: 10.9,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Küchenrolle Recycling 4er",
|
||||
quantity: 1,
|
||||
price: 4.74,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Spülmittel Lemon 500ml",
|
||||
quantity: 1,
|
||||
price: 2.43,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Mineralwasser Medium 1.0l",
|
||||
quantity: 1,
|
||||
price: 2.5,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Verpflegungsmehraufwand",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-rewe-scen-2",
|
||||
originalFileName: "04_rewe_supermarkt_kassenbon.jpg",
|
||||
fileSizeBytes: 210000,
|
||||
createdAt: "2026-08-15T18:20:00.000Z",
|
||||
updatedAt: "2026-08-15T18:20:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(reweReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
expect(val.calculatedTaxSum).toBe(2.84); // 1.63 + 1.21
|
||||
|
||||
const buf = await generateDualSheetExcel([reweReceipt]);
|
||||
const wb = new ExcelJS.Workbook();
|
||||
await wb.xlsx.load(buf as any);
|
||||
const s2 = wb.getWorksheet("Einzelpositionen Detail");
|
||||
expect(s2?.rowCount).toBe(9); // 1 header + 8 line items
|
||||
});
|
||||
|
||||
// Scenario 3: Trattoria Bella Vista Bewirtungsbeleg with Tip & Attendees (§4 Abs. 5 EStG)
|
||||
test("Scenario 3: Trattoria Bella Vista Bewirtungsbeleg with Tip & Attendees (§4 Abs. 5 EStG)", async () => {
|
||||
const trattoriaReceipt: ProcessedReceipt = {
|
||||
id: "scen-3-trattoria",
|
||||
merchant: {
|
||||
name: "Trattoria Bella Vista Ristorante",
|
||||
address: "Marktplatz 12, 10115 Berlin",
|
||||
taxId: "DE987654321",
|
||||
confidence: 0.96,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "21:10",
|
||||
confidence: 0.95,
|
||||
},
|
||||
documentType: "BEWIRTUNGSBELEG",
|
||||
receiptNumber: "TR-2026-9941",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 125.5,
|
||||
confidence: 0.97,
|
||||
},
|
||||
netAmount: 104.97,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 16.03,
|
||||
netAmount: 84.37,
|
||||
},
|
||||
{
|
||||
ratePercent: 7,
|
||||
taxAmount: 4.5,
|
||||
netAmount: 20.6,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "2x Tagliolini al Tartufo Nero",
|
||||
quantity: 2,
|
||||
price: 52.0,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "1x Filetto di Manzo 250g",
|
||||
quantity: 1,
|
||||
price: 36.5,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "1x San Pellegrino 0.75l",
|
||||
quantity: 1,
|
||||
price: 7.5,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "1x Chianti Classico DOCG Flasche",
|
||||
quantity: 1,
|
||||
price: 28.0,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Trinkgeld / Tip (steuerfrei)",
|
||||
quantity: 1,
|
||||
price: 1.5,
|
||||
taxRate: null,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Bewirtung",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-trattoria-scen-3",
|
||||
originalFileName: "02_trattoria_bewirtungsbeleg_berlin.jpg",
|
||||
fileSizeBytes: 290000,
|
||||
createdAt: "2026-08-15T21:10:00.000Z",
|
||||
updatedAt: "2026-08-15T21:10:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(trattoriaReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
|
||||
const csv = generateAccountingCsv([trattoriaReceipt]);
|
||||
expect(csv).toContain('"Trattoria Bella Vista Ristorante - Bewirtung"');
|
||||
expect(csv).toContain('"125,50"');
|
||||
});
|
||||
|
||||
// Scenario 4: MediaMarkt Saturn IT Equipment Invoice
|
||||
test("Scenario 4: MediaMarkt Saturn IT Equipment Invoice (Hardware, Serial Numbers, 19% VAT)", async () => {
|
||||
const itReceipt: ProcessedReceipt = {
|
||||
id: "scen-4-mediamarkt",
|
||||
merchant: {
|
||||
name: "MediaMarkt Saturn Holding",
|
||||
address: "Alexanderplatz 3, 10178 Berlin",
|
||||
taxId: "DE119876543",
|
||||
confidence: 0.99,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "15:45",
|
||||
confidence: 0.98,
|
||||
},
|
||||
documentType: "RECHNUNG",
|
||||
receiptNumber: "MM-INV-2026-77812",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 299.98,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 252.08,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 47.9,
|
||||
netAmount: 252.08,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "Dell USB-C 4K Triple Display Dock 130W",
|
||||
quantity: 1,
|
||||
price: 189.99,
|
||||
taxRate: 19,
|
||||
},
|
||||
{
|
||||
description: "Logitech MX Keys S Tastatur Wireless",
|
||||
quantity: 1,
|
||||
price: 109.99,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Bürobedarf & IT",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-mediamarkt-scen-4",
|
||||
originalFileName: "03_mediamarkt_it_rechnung.jpg",
|
||||
fileSizeBytes: 310000,
|
||||
createdAt: "2026-08-15T15:45:00.000Z",
|
||||
updatedAt: "2026-08-15T15:45:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(itReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
|
||||
const buf = await generateDualSheetExcel([itReceipt]);
|
||||
const wb = new ExcelJS.Workbook();
|
||||
await wb.xlsx.load(buf as any);
|
||||
const s1 = wb.getWorksheet("Belegübersicht");
|
||||
expect(s1?.getRow(2).getCell(5).value).toBe("RECHNUNG");
|
||||
expect(s1?.getRow(2).getCell(10).value).toBe(299.98);
|
||||
});
|
||||
|
||||
// Scenario 5: Hotel Übernachtung + Frühstück Split (7% vs 19% + City Tax 0%)
|
||||
test("Scenario 5: Hotel Übernachtung + Frühstück Split (Lodging 7% vs Breakfast 19% + City Tax 0%)", async () => {
|
||||
const hotelReceipt: ProcessedReceipt = {
|
||||
id: "scen-5-hotel",
|
||||
merchant: {
|
||||
name: "Motel One München Sendlinger Tor",
|
||||
address: "Herzog-Wilhelm-Str. 28, 80331 München",
|
||||
taxId: "DE263819201",
|
||||
confidence: 0.98,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-14",
|
||||
time: "08:15",
|
||||
confidence: 0.96,
|
||||
},
|
||||
documentType: "RECHNUNG",
|
||||
receiptNumber: "MO-MUC-88412",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 129.5,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 118.84,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 7,
|
||||
taxAmount: 7.0,
|
||||
netAmount: 100.0, // Lodging
|
||||
},
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 3.66,
|
||||
netAmount: 19.26, // Breakfast & Business Package
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "1x Übernachtung Standard Room (13.08.-14.08.)",
|
||||
quantity: 1,
|
||||
price: 107.0,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "1x Bio-Frühstücksbuffet & WLAN Business",
|
||||
quantity: 1,
|
||||
price: 22.5,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Reisekosten & Hotel",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-hotel-scen-5",
|
||||
originalFileName: "hotel_motel_one.jpg",
|
||||
fileSizeBytes: 260000,
|
||||
createdAt: "2026-08-14T08:15:00.000Z",
|
||||
updatedAt: "2026-08-14T08:15:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(hotelReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
|
||||
const csv = generateAccountingCsv([hotelReceipt]);
|
||||
expect(csv).toContain('"Motel One München Sendlinger Tor"');
|
||||
expect(csv).toContain('"129,50"');
|
||||
expect(csv).toContain('"7,00"');
|
||||
expect(csv).toContain('"3,66"');
|
||||
});
|
||||
|
||||
// Scenario 6: Taxi Deutschland Urban Ride (7% VAT according to §12 Abs. 2 Nr. 10 UStG)
|
||||
test("Scenario 6: Taxi Deutschland Urban Ride (7% VAT for local transport <= 50km)", async () => {
|
||||
const taxiReceipt: ProcessedReceipt = {
|
||||
id: "scen-6-taxi",
|
||||
merchant: {
|
||||
name: "Taxi Funk München eG - Wagen 312",
|
||||
address: "Heimeranstr. 35, 80339 München",
|
||||
taxId: "DE129554411",
|
||||
confidence: 0.97,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "23:45",
|
||||
confidence: 0.95,
|
||||
},
|
||||
documentType: "KASSENBON",
|
||||
receiptNumber: "TX-9901-26",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 28.5,
|
||||
confidence: 0.98,
|
||||
},
|
||||
netAmount: 26.64,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 7,
|
||||
taxAmount: 1.86,
|
||||
netAmount: 26.64,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "Taxifahrt Stadtgebiet München (8.4 km)",
|
||||
quantity: 1,
|
||||
price: 28.5,
|
||||
taxRate: 7,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Reisekosten & Hotel",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-taxi-scen-6",
|
||||
originalFileName: "taxi_muenchen.jpg",
|
||||
fileSizeBytes: 175000,
|
||||
createdAt: "2026-08-15T23:45:00.000Z",
|
||||
updatedAt: "2026-08-15T23:45:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(taxiReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
expect(val.calculatedTaxSum).toBe(1.86);
|
||||
|
||||
const buf = await generateDualSheetExcel([taxiReceipt]);
|
||||
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.86);
|
||||
expect(s1?.getRow(2).getCell(9).value).toBe(0.0);
|
||||
});
|
||||
|
||||
// Scenario 7: APCOA Parkhaus Ticket (19% VAT)
|
||||
test("Scenario 7: APCOA Parkhaus Ticket (Short-term parking, 19% VAT)", async () => {
|
||||
const parkingReceipt: ProcessedReceipt = {
|
||||
id: "scen-7-apcoa",
|
||||
merchant: {
|
||||
name: "APCOA Parking Deutschland GmbH",
|
||||
address: "Parkhaus Marienplatz, 80331 München",
|
||||
taxId: "DE147852369",
|
||||
confidence: 0.99,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "14:10",
|
||||
confidence: 0.98,
|
||||
},
|
||||
documentType: "PARKTICKET",
|
||||
receiptNumber: "PK-APCOA-4412",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 14.0,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 11.76,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 2.24,
|
||||
netAmount: 11.76,
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "Parkzeit 3 Std. 15 Min. (Tarif Standard)",
|
||||
quantity: 1,
|
||||
price: 14.0,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Tanken & KFZ",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-apcoa-scen-7",
|
||||
originalFileName: "apcoa_parking.jpg",
|
||||
fileSizeBytes: 140000,
|
||||
createdAt: "2026-08-15T14:10:00.000Z",
|
||||
updatedAt: "2026-08-15T14:10:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(parkingReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
|
||||
const csv = generateAccountingCsv([parkingReceipt]);
|
||||
expect(csv).toContain('"APCOA Parking Deutschland GmbH"');
|
||||
expect(csv).toContain('"14,00"');
|
||||
expect(csv).toContain('"2,24"');
|
||||
});
|
||||
|
||||
// Scenario 8: Bäckerei / Backstube Small Cash Receipt (7% Baked Goods + 19% Coffee)
|
||||
test("Scenario 8: Bäckerei Small Cash Receipt (7% take-away bread + 19% on-site cappuccino)", async () => {
|
||||
const bakeryReceipt: ProcessedReceipt = {
|
||||
id: "scen-8-bakery",
|
||||
merchant: {
|
||||
name: "Bio-Bäckerei Hofpfisterei GmbH",
|
||||
address: "Viktualienmarkt 8, 80331 München",
|
||||
taxId: "DE128844332",
|
||||
confidence: 0.98,
|
||||
},
|
||||
date: {
|
||||
isoDate: "2026-08-15",
|
||||
time: "08:45",
|
||||
confidence: 0.97,
|
||||
},
|
||||
documentType: "KASSENBON",
|
||||
receiptNumber: "HPF-2026-1192",
|
||||
currency: "EUR",
|
||||
totalAmount: {
|
||||
value: 8.9,
|
||||
confidence: 0.99,
|
||||
},
|
||||
netAmount: 7.97,
|
||||
taxBreakdown: [
|
||||
{
|
||||
ratePercent: 7,
|
||||
taxAmount: 0.35,
|
||||
netAmount: 4.95, // 5.30€ baked goods take-away
|
||||
},
|
||||
{
|
||||
ratePercent: 19,
|
||||
taxAmount: 0.58,
|
||||
netAmount: 3.02, // 3.60€ Cappuccino
|
||||
},
|
||||
],
|
||||
lineItems: [
|
||||
{
|
||||
description: "Pfister Öko-Landbrot 1kg",
|
||||
quantity: 1,
|
||||
price: 5.3,
|
||||
taxRate: 7,
|
||||
},
|
||||
{
|
||||
description: "Cappuccino Groß (im Haus)",
|
||||
quantity: 1,
|
||||
price: 3.6,
|
||||
taxRate: 19,
|
||||
},
|
||||
],
|
||||
suggestedCategory: "Verpflegungsmehraufwand",
|
||||
validation: {
|
||||
isMathValid: true,
|
||||
isDuplicateSuspected: false,
|
||||
needsUserReview: false,
|
||||
reviewField: "none",
|
||||
reviewReason: null,
|
||||
},
|
||||
imageHash: "hash-bakery-scen-8",
|
||||
originalFileName: "hofpfisterei_beleg.jpg",
|
||||
fileSizeBytes: 155000,
|
||||
createdAt: "2026-08-15T08:45:00.000Z",
|
||||
updatedAt: "2026-08-15T08:45:00.000Z",
|
||||
status: "ready",
|
||||
};
|
||||
|
||||
const val = validateReceiptMath(bakeryReceipt);
|
||||
expect(val.isMathValid).toBe(true);
|
||||
|
||||
const buf = await generateDualSheetExcel([bakeryReceipt]);
|
||||
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(8.9);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user