Intelligent invoice extraction, validation, and export system for mass payment processing compatible with Swiss and European ebanking platforms (BKB, Raiffeisen, UBS, BLKB, PostFinance, etc.).
- PDF Upload + Swiss QR Scan — Accepts PDF invoices; instantly decodes Swiss QR-bill (QR-Rechnung / SPC) codes where present
- Two-Stage LLM Extraction — For non-QR invoices: text-layer extraction first (fast/cheap), then image-rendering fallback for scanned PDFs — both using Claude Haiku
- Vendor IBAN Lookup — Cross-references extracted receiver names against a local vendor database; autofills or flags IBAN mismatches
- Web Review Interface — Modal-based editing with field validation; inline bank assignment
- Dual-Bank Export Screen — Drag-and-drop invoices to BKB or Raiffeisen columns; gated export with blocker popup
- ISO 20022 pain.001 Export — Bank-sorted XML files for direct upload to ebanking portals
- CSV Export — Spreadsheet export with all payment fields
- Analytics Dashboard — Breakdown of extraction methods (QR / text / image / hybrid)
Lyfegen HealthTech AG — Built for internal invoice mass-payment automation.
- Docker or Podman (5.0+)
- Anthropic API key — Claude Haiku is used for all LLM extraction
# Clone the repo
git clone <repo-url>
cd invoice-processor
# Create .env from template
cp .env.example .env
# Edit .env — set ANTHROPIC_API_KEY and DEBTOR_* fields at minimum
# Build and run
podman compose up --build -d
# Server runs on http://localhost:8080# Required: Anthropic API key for Claude Haiku extraction
ANTHROPIC_API_KEY=sk-ant-...
# Optional: override Claude model (default: claude-haiku-4-5-20251001)
LLM_MODEL=claude-haiku-4-5-20251001
LLM_MODEL_TEXT=claude-haiku-4-5-20251001
# Pain.001 sender (debtor) — your company
DEBTOR_NAME=Your Company AG
DEBTOR_IBAN=CH56...
DEBTOR_BIC=BLKBCH22
# Development: enables startup self-tests on container boot
DEV_MODE=true
# Storage paths (defaults work inside container)
DB_PATH=/app/data/invoices.db
UPLOAD_DIR=/app/data/uploads
# Optional: save QR scan debug images
DEBUG_QR_DIR=/tmp/qr_debugThe app uses a named Docker/Podman volume to avoid OneDrive sync conflicts:
volumes:
invoice_processor_data: # Stores uploads/ and invoices.dbpodman volume inspect invoice_processor_dataPDF Upload
│
├─ Swiss QR-bill present? ──► QR-processed (done instantly)
│ (qr_swiss.py: pyzbar/zxingcpp decode)
│
└─ No QR ──► LLM-Pending
│
▼ (background batch — POST /api/run-llm-batch)
Stage 1: Text layer (fitz → Claude Haiku)
│
Vendor check: autofill IBAN from vendor DB
│
Complete? ──► LLM-Done
│
Incomplete? ──► Stage 2: Image render (fitz → PNG → Claude Haiku)
│
Final vendor check
│
All mandatory fields? ──► LLM-Done
│
Missing fields? ──► needs_review
│
▼
Web Review Modal (manual editing)
│
▼
Export Screen (drag BKB ↔ Raiffeisen)
│
▼
pain.001 XML + archive
Scans each PDF page at multiple DPIs (150/300/400) and crop strategies:
- Full page scan
- Bottom-third crop (Swiss QR slip is always in the bottom section)
- Decoders: pyzbar (primary) + zxingcpp (fallback)
- Parses SPC payload (SIX standard 2.2): IBAN, receiver, amount, currency, reference
Uses Claude Haiku exclusively (no Ollama, no local models):
| Stage | Method | When |
|---|---|---|
| Text | fitz text layer → Haiku JSON | PDF has ≥80 chars of text |
| Image | fitz → PNG base64 → Haiku (prefill {) |
Text stage incomplete or no text layer |
Both stages return the same JSON schema:
{"invoice_id": "...", "receiver": "...", "amount": "1234.56",
"currency": "CHF", "due_date": "2026-07-01", "iban": "CH56...",
"bic": "BLKBCH22", "reference": "..."}match_type values: text_full | image_only | hybrid | failed
Case-insensitive exact match on receiver_name:
- Match → autofill IBAN/BIC from DB (
iban_source = "database") - Match + IBAN present → confirm (
iban_source = "document") or flag mismatch (iban_source = "document_mismatch") - Operator saves review →
iban_source = "manual"
| Currency | Bank |
|---|---|
| CHF, SEK, EUR | BKB |
| USD, CAD, GBP | RAIFFEISEN |
| Other | MANUAL |
Generates pain.001.001.03 XML, one file per bank, grouped by currency:
| Currency | Bank | Service Level | Local Instrument |
|---|---|---|---|
| CHF | BKB | NURG | — |
| SEK | BKB | NURG | SWIFT |
| EUR | BKB | SEPA | — |
| USD/CAD/GBP | RAIFFEISEN | NURG | SWIFT |
- Uses
decimal.Decimalfor all monetary sums (no float drift) - Handles European decimal comma (
"1234,50"→1234.50)
Two screens:
Dashboard (/):
- Drag-drop upload (multi-PDF)
- Job table with status badges and IBAN source chips
- Search and sort
- Modal review editor with inline PDF viewer
- Run AI batch button
Export Screen (/export):
- Drag-and-drop between BKB / Raiffeisen / Unsorted columns
- Blocker popup: shows invoices that need review before export
- Download button: generates both pain.001 files as a ZIP + archives sorted jobs
| ID | Tests |
|---|---|
| T1 | IBAN MOD-97 checksum (valid, invalid, CH fake, zero-width space) |
| T2 | BIC regex (8-char, 11-char, fake patterns) |
| T4 | pain.001 XML well-formedness + GrpHdr + NbOfTxs + creditor IBAN |
| T5 | Multi-currency → separate PmtInf blocks |
| T6 | No-IBAN jobs skipped; IBAN-only (no BIC) included |
| T7 | Bank routing: CHF→BKB, USD→RAIFFEISEN, XYZ→MANUAL |
| T8 | run_qr: valid SPC → fields; no-QR → LLM-Pending |
| T9 | IBAN validation edge cases |
| T10 | Amount parsing: EU comma, thousands separator |
| T11 | Vendor lookup: autofill, mismatch, unknown vendor |
| Method | Path | Description |
|---|---|---|
GET |
/ |
Dashboard page |
GET |
/export |
Export screen |
POST |
/api/upload |
Upload PDF(s), sync QR scan, persist |
GET |
/api/jobs |
List jobs (?include_archived=true) |
POST |
/api/review/{id} |
Save edited fields from review modal |
POST |
/api/run-llm-batch |
Trigger background LLM extraction |
POST |
/api/assign-bank/{id} |
Override bank_target |
GET |
/api/pdf/{id} |
Serve original PDF |
GET |
/api/analytics |
Extraction method breakdown |
GET |
/api/vendors |
List vendors |
POST |
/api/vendors |
Create vendor |
PUT |
/api/vendors/{id} |
Update vendor |
DELETE |
/api/vendors/{id} |
Delete vendor |
GET |
/api/export-readiness |
Check for blockers |
POST |
/download/confirm |
Generate ZIP (pain.001 ×2) + archive |
GET |
/download/csv |
CSV export |
DELETE |
/api/jobs/{id} |
Hard-delete job + file |
DELETE |
/api/clear-all |
Wipe all non-archived jobs |
invoices.db (SQLite):
CREATE TABLE jobs (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'LLM-Pending',
-- Extracted payment fields
receiver TEXT DEFAULT '',
iban TEXT DEFAULT '',
bic TEXT DEFAULT '',
amount TEXT DEFAULT '',
currency TEXT DEFAULT '',
reference TEXT DEFAULT '',
invoice_id TEXT DEFAULT '',
-- Routing & metadata
bank_target TEXT DEFAULT '', -- BKB | RAIFFEISEN | MANUAL
iban_source TEXT DEFAULT '', -- document | database | document_mismatch | llm | manual
iban_mismatch_db TEXT DEFAULT '', -- vendor DB IBAN when mismatch detected
match_type TEXT DEFAULT '', -- text_full | image_only | hybrid | failed
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE vendors (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
receiver_name TEXT NOT NULL,
iban TEXT NOT NULL,
bic TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);Job status enum: QR-processed | LLM-Pending | LLM-Done | needs_review | archived | error
| Package | Purpose |
|---|---|
| fastapi | Web framework + REST API |
| uvicorn | ASGI server |
| anthropic | Claude Haiku API client |
| pymupdf (fitz) | PDF text extraction + image rendering |
| pyzbar | Primary QR/barcode decoder |
| zxingcpp | Fallback QR decoder |
| Pillow | Image processing for QR scan |
| python-multipart | Multipart file upload |
| jinja2 | HTML templating |
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # Mac/Linux
pip install -r requirements.txt
# Set env vars
set ANTHROPIC_API_KEY=sk-ant-...
set DEBTOR_NAME=Test Corp
set DEBTOR_IBAN=CH5604835012345678009
set DEBTOR_BIC=BLKBCH22
set DEV_MODE=true
# Run
uvicorn app.main:app --reload --port 8000The volume keeps data outside the synced project directory:
podman volume ls | findstr invoice_processor
podman volume inspect invoice_processor_data- Check
ANTHROPIC_API_KEYis set and valid - Check container logs:
podman compose logs -f app - Force re-run: click "✦ Run AI" on the dashboard
- Invoice may not be a Swiss QR-Rechnung
- Enable debug mode: set
DEBUG_QR_DIR=/tmp/qr_debugand inspect saved images - QR code may be too small — the scanner tries 150/300/400 DPI automatically
Uploads and database are stored in a named volume, not the project directory — no sync conflicts.
Proprietary — Lyfegen HealthTech AG