Kaufland-Bon-Import gebaut: PDF + KI (07.06.2026, ADR-52)
Executive Summary
Kaufland-Kassenbons landen jetzt im Archiv/Vorrat — über PDF-Upload + KI-Extraktion, nicht über die App-API. Grund: Dennis' mitmproxy-Session (analog eGYM) zeigte, dass die Kaufland-App Certificate Pinning betreibt (app.kaufland.net + account.kaufland.com brechen den Handshake ab, während 1Password/Amazon/Outlook sauber durch denselben Proxy laufen). Der passive Auto-Pull (Phase B) wäre damit nur über App-Repacking/Frida machbar — vertagt. Stattdessen Phase A: Bon-PDF aus der Kaufland-Card-App exportieren → in Silverscale hochladen → fertig.
In einfachen Worten
In der Kaufland-App den digitalen Bon als PDF teilen, in Silverscale unter „Einkäufe" auf 📄 Bon-PDF tippen, hochladen. Die KI liest den Bon (alle Positionen, Preise, Rabatte, Pfand), legt ihn ins Bon-Archiv und bucht den Vorrat — genau wie ein Lidl-Bon, nur eben ohne Automatik. Nach ≤1 Minute taucht er in der Liste auf.
Architektur
📄 PDF → POST /api/receipts/import-pdf (Raw-Body, %PDF-Check, 15 MB)
└─ queut ai_job "receipt_pdf"
→ ai_jobs.job_receipt_pdf (Host-Worker, minütlich)
└─ claude-CLI + Read-Tool liest PDF → striktes Positions-JSON
→ POST /api/receipts/import (das JSON)
└─ receipt_import.import_receipt_dict():
· Food-Matching über NAMEN (NFKD-normalisiert):
bestehenden Artikel wiederverwenden, sonst neu (store='Kaufland')
· Pfand/Leergut → is_deposit (kein food)
· Idempotenz: kfl-<sha1(store|datum|summe)>
· Auto-Bestand wie Lidl
· Kontrollsumme Σ Zeilen == Endbetrag → needs_review-Flag
Bewusst getrennt von importer.py: Der Lidl-Importer ist artId-zentriert (Lidl-Artikelnummern). Fremd-Bons haben die nicht → eigener, namensbasierter Pfad in receipt_import.py. Das Modul ist quelle-agnostisch: ein künftiger dritter Markt bräuchte nur einen Extraktions-Prompt, kein neues Import-Backend.
Sicherheit & Qualität
import-pdfprüft den%PDF--Header und deckelt bei 15 MB.- Kontrollsumme centgenau (wie beim Lidl-HTML-Parser): weicht Σ aller Zeilen vom Bon-Endbetrag um >2 ct ab, kommt der Bon mit
needs_review-Flag — die KI kann sich verlesen, das Flag macht es sichtbar. - Idempotent: derselbe Bon zweimal hochgeladen erzeugt kein Duplikat (deterministische ID aus Markt+Datum+Summe).
- 6 neue Contract-Tests in der API-Suite (ADR-51): Neu-Import, Idempotenz, Kontrollsummen-Review, kaputtes Dict (400), PDF-Queue, Nicht-PDF (400) — Gate grün.
Vorschau vor dem Import (Nachtrag, gleicher Tag)
Damit ein verlesener Bon nicht still die Datenbank verändert, importiert der PDF-Weg nicht mehr automatisch, sondern zeigt erst eine Vorschau:
📄 PDF hochladen → job_receipt_pdf EXTRAHIERT nur (KI → Bon-JSON in Job-note),
importiert selbst NICHTS
↓ Frontend pollt den Job (AiProgress-Ring)
📋 Vorschau-Sheet (ReceiptPreview.svelte): erkannte Positionen, Preise,
Rabatte, Pfand; Kontrollsummen-Warnung falls Σ ≠ Endbetrag
· jede Food-Position per Checkbox fürs Einlagern abwählbar (Pfand fix aus)
↓ „Importieren" ↓ „Verwerfen"
POST /api/receipts/import (mit to_inventory nichts passiert –
je Zeile) → Bon ins Archiv + gewählte in der DB lag bis hierhin
Positionen in den Vorrat ohnehin nichts
- Bis zum Klick auf „Importieren" landet nichts in der DB. Verwerfen lässt den Bon spurlos fallen.
- Pro Position steuert
receipt_items[].to_inventory(Defaulttrue), ob die Zeile den Bestand bucht. Der Bon kommt immer komplett ins Archiv – nur das Einlagern ist abwählbar. Pfand/Leergut wird nie eingelagert. - Die Kontrollsumme (Σ Zeilen vs. Endbetrag) erscheint als ⚠️ schon in der Vorschau – man sieht einen KI-Verleser, bevor man bestätigt.
- 1 weiterer Contract-Test (
to_inventory=False→ Position nicht im Vorrat, Bon trotzdem vollständig im Archiv); der Worker liefert jetzt nur noch das extrahierte JSON.
Phase B (vertagt)
Auto-Pull via kaufland.py braucht App-Entpinnung (iOS-IPA + Frida + Re-Signing mit Dev-Account, oder Android-Emulator + apk-mitm). Details + Plan-C in der Recherche §8. Bei echtem Bedarf jederzeit nachrüstbar — Phase A deckt den Alltag risikofrei ab.
Bezug: Kaufland-Recherche · ADR-52 in app/ARCHITEKTUR.md