Modul 11 — WhatsApp + OCR-Ausweis
1. Zweck & Scope
Dieses Modul digitalisiert den KYC-Prozess (Know Your Customer) für Dominicano Express: Kunden oder Mitarbeitende fotografieren ein Identitätsdokument (dominikanische Cédula de Identidad oder Reisepass) entweder direkt in der Caja-App per Handy-Kamera oder senden das Bild via WhatsApp. Ein self-hosted OCR-/Vision-Dienst auf eigener Proxmox-Infra (Entscheid 2026-06-28 Teil 3 — kein Cloud-/Drittland-Transfer der Ausweisbilder fürs OCR) extrahiert strukturiert Name, Vorname, Geburtsdatum, Dokumentnummer, Nationalität und (falls lesbar) Adresse und befüllt den Kontaktsatz in contacts vor oder aktualisiert ihn. Das Modul dient primär dem Zoll-/Empfänger-Matching in der Dominikanischen Republik (Sendungsmodul), der Lead-Qualifikation und der revDSG-konformen Identitätsprüfung. Explizit nicht im Scope: automatisierte Behörden-Datenbankabfragen, Echtzeit-Bonitätsprüfungen, Video-Ident.
2. Domänenmodell
Entitäten & Beziehungen
contacts (1) ──────────── (0..n) kyc_scans
│
├── document_type (CEDULA | PASSPORT | OTHER)
├── extracted_fields (JSONB)
├── raw_image_path (Storage-Ref)
├── ocr_provider (ENUM)
├── ocr_confidence (float)
├── review_status (ENUM)
└── reviewed_by → auth.users
whatsapp_inbound (0..n) ──── (0..1) kyc_scans (eingehende WA-Medien → Scan)
kyc_scans (0..1) ──── (0..1) contacts (nach manueller Bestätigung)
ER-Skizze (Liste)
contacts— Ziel-Entität; wird durch Scan vorausgefüllt oder ergänztkyc_scans— ein Scan-Versuch; enthält Rohdaten + extrahierte Felder + Review-Statuswhatsapp_inbound— Eingangs-Queue für WhatsApp-Nachrichten (Medien + Metadaten)kyc_field_corrections— Audit-Journal für manuelle Korrekturen an extrahierten Feldernaudit_log— jede KYC-Aktion wird revisionssicher protokolliert (Plattform-Modul)
3. Supabase-Schema
3.1 Enum-Definitionen
-- Dokumenttypen (stabile Codes, i18n über kyc_document_types-Lookup)
CREATE TYPE kyc_doc_type AS ENUM ('CEDULA', 'PASSPORT', 'OTHER');
-- OCR-Anbieter (erweiterbar ohne Migration via Lookup-Tabelle)
-- Entscheid 2026-06-28 (Teil 3): OCR läuft SELF-HOSTED auf eigener Proxmox-Infra —
-- KEIN Cloud-/Drittland-Transfer der Ausweisbilder fürs OCR. Default = 'SELF_HOSTED'.
-- Cloud-Codes bleiben nur als (nicht genutzte) Fallback-Option im Enum erhalten.
CREATE TYPE kyc_ocr_provider AS ENUM (
'SELF_HOSTED', -- self-hosted Vision/OCR auf eigener Proxmox-Infra (Bild verlässt die Infra NICHT); Stack = Qwen3-VL-8B-Instruct (primär) + PaddleOCR-VL (Fallback), siehe §8 + §10
'ANTHROPIC_VISION', -- (nicht genutzt) Claude Vision — Cloud/Drittland
'GOOGLE_VISION', -- (nicht genutzt) Google Cloud Vision API — Cloud
'AZURE_FORM', -- (nicht genutzt) Azure AI Document Intelligence — Cloud
'MANUAL' -- manuell erfasst ohne OCR
);
-- Review-Status eines Scans
CREATE TYPE kyc_review_status AS ENUM (
'PENDING', -- OCR abgeschlossen, wartet auf manuelle Prüfung
'APPROVED', -- Felder bestätigt, in contacts übertragen
'REJECTED', -- Scan abgelehnt (unlesbar, falsches Dokument etc.)
'MERGED' -- Felder in bestehenden contacts-Satz gemergt
);
-- WhatsApp-Nachrichten-Status
CREATE TYPE wa_inbound_status AS ENUM (
'RECEIVED', -- eingetroffen, noch nicht verarbeitet
'PROCESSING', -- OCR läuft
'LINKED', -- kyc_scan erzeugt + ggf. Kontakt verknüpft
'IGNORED' -- kein Dokument (z. B. Text-Nachricht)
);
3.2 Lookup-Tabelle: kyc_document_types
CREATE TABLE kyc_document_types (
code kyc_doc_type PRIMARY KEY,
label_de TEXT NOT NULL,
label_es TEXT NOT NULL,
label_en TEXT NOT NULL,
description TEXT
);
INSERT INTO kyc_document_types VALUES
('CEDULA', 'Dominikanischer Personalausweis', 'Cédula de Identidad', 'Dominican ID card', NULL),
('PASSPORT', 'Reisepass', 'Pasaporte', 'Passport', NULL),
('OTHER', 'Sonstiges Dokument', 'Otro documento', 'Other document', NULL);
F-19 — Lookup-RLS:
kyc_document_typesfolgt (wie alle Lookups) der zentralen RLS-Policy in Dok 30 §13 (CRUfür ADMIN,Rfür alle übrigen Rollen).enable RLS+ Policy in derselben Migration anlegen.
3.3 Tabelle: whatsapp_inbound
CREATE TABLE whatsapp_inbound (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
wa_message_id TEXT NOT NULL UNIQUE, -- WhatsApp Cloud API message_id
wa_from TEXT NOT NULL, -- E.164-Nummer des Absenders
wa_account_id TEXT NOT NULL, -- WABA-ID (für Multi-Account-Zukunft)
media_type TEXT, -- 'image/jpeg', 'image/png', 'application/pdf'
media_url TEXT, -- temporäre WA-Download-URL (24h gültig)
storage_path TEXT, -- eigener Supabase-Storage-Pfad nach Download
body_text TEXT, -- Begleittext der Nachricht (optional)
status wa_inbound_status NOT NULL DEFAULT 'RECEIVED',
kyc_scan_id UUID REFERENCES kyc_scans(id), -- gesetzt wenn Scan erzeugt
contact_id UUID REFERENCES contacts(id), -- gesetzt wenn Absender erkannt
error_message TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_wa_inbound_from ON whatsapp_inbound(wa_from);
CREATE INDEX idx_wa_inbound_status ON whatsapp_inbound(status);
CREATE INDEX idx_wa_inbound_received ON whatsapp_inbound(received_at DESC);
RLS whatsapp_inbound:
admin(Marcel, Mariela, Markus): SELECT, UPDATE (Status-Änderung, manuelles Linking)ops(Markus): SELECT, UPDATEdriver(Arkys): kein Zugriffaffiliate: kein Zugriff- Service-Role (Edge Function Webhook): INSERT, UPDATE (via service_role key, kein RLS)
3.4 Tabelle: kyc_scans
CREATE TABLE kyc_scans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
contact_id UUID REFERENCES contacts(id), -- NULL bis Merge
wa_inbound_id UUID REFERENCES whatsapp_inbound(id), -- NULL bei Kamera-Upload
document_type kyc_doc_type NOT NULL DEFAULT 'CEDULA',
raw_image_path TEXT NOT NULL, -- Supabase Storage: kyc-documents/<contact_id>/<uuid>.jpg
ocr_provider kyc_ocr_provider NOT NULL DEFAULT 'SELF_HOSTED', -- Entscheid 2026-06-28 Teil 3: self-hosted (Proxmox), kein Cloud-Transfer
ocr_confidence NUMERIC(4,3), -- 0.000–1.000; NULL wenn manuell
ocr_raw_response JSONB, -- vollständige Provider-Antwort (für Re-Analyse)
extracted_fields JSONB NOT NULL DEFAULT '{}',
-- extracted_fields Struktur (Beispiel):
-- {
-- "first_name": "Juan",
-- "last_name": "García Pérez",
-- "doc_number": "001-1234567-8",
-- "birth_date": "1985-03-22",
-- "nationality": "DO",
-- "address": "Calle 5 No. 10, Santo Domingo",
-- "expiry_date": "2028-03-22",
-- "gender": "M",
-- "confidence_per_field": {"first_name": 0.97, "doc_number": 0.91, ...}
-- }
review_status kyc_review_status NOT NULL DEFAULT 'PENDING',
reviewed_by UUID REFERENCES auth.users(id),
reviewed_at TIMESTAMPTZ,
rejection_reason TEXT, -- bei REJECTED
notes TEXT,
source TEXT NOT NULL DEFAULT 'CAMERA', -- 'CAMERA' | 'WHATSAPP'
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_kyc_scans_contact ON kyc_scans(contact_id);
CREATE INDEX idx_kyc_scans_doc_number ON kyc_scans((extracted_fields->>'doc_number'));
CREATE INDEX idx_kyc_scans_review_status ON kyc_scans(review_status);
CREATE INDEX idx_kyc_scans_created ON kyc_scans(created_at DESC);
-- Soft-Unique: pro Kontakt sollte max. 1 aktiver (APPROVED) Scan pro Dokumenttyp existieren
CREATE UNIQUE INDEX idx_kyc_scans_approved_per_contact_doc
ON kyc_scans(contact_id, document_type)
WHERE review_status = 'APPROVED';
RLS kyc_scans:
admin: SELECT, INSERT, UPDATE, DELETE (inkl. Review-Aktionen)ops: SELECT, INSERT, UPDATE (kein DELETE)driver: INSERT (nur eigener Kamera-Upload für zugewiesenen Auftrag), SELECT (eigene Scans)affiliate: kein Zugriffreadonly: kein Zugriff (Ausweisdaten besonders schützenswert)
3.5 Tabelle: kyc_field_corrections
CREATE TABLE kyc_field_corrections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kyc_scan_id UUID NOT NULL REFERENCES kyc_scans(id) ON DELETE CASCADE,
field_name TEXT NOT NULL, -- z. B. 'first_name', 'doc_number'
ocr_value TEXT, -- ursprünglicher OCR-Wert
corrected_value TEXT NOT NULL, -- manuell korrigierter Wert
corrected_by UUID NOT NULL REFERENCES auth.users(id),
corrected_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_kyc_corrections_scan ON kyc_field_corrections(kyc_scan_id);
RLS kyc_field_corrections:
admin,ops: SELECT, INSERT (Korrekturen erstellen), kein DELETE (revisionssicher)- alle anderen: kein Zugriff
3.6 Erweiterungen in contacts
-- Zu ergänzende Spalten in der contacts-Tabelle (aus kyc_scans befüllt):
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS cedula_number TEXT UNIQUE;
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS passport_number TEXT;
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS kyc_status TEXT DEFAULT 'NONE'
CHECK (kyc_status IN ('NONE', 'PENDING', 'VERIFIED', 'REJECTED'));
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS kyc_scan_id UUID REFERENCES kyc_scans(id);
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS kyc_verified_at TIMESTAMPTZ;
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS birth_date DATE;
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS nationality CHAR(2); -- ISO 3166-1 alpha-2
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS address_dr TEXT; -- Zustelladresse DR
3.7 Storage-Bucket: kyc-documents
-- Konfiguration (via Supabase Dashboard / Migration):
-- Bucket: kyc-documents
-- Public: FALSE (privat, kein öffentlicher URL-Zugriff)
-- Allowed MIME types: image/jpeg, image/png, image/webp, application/pdf
-- Max file size: 10 MB
-- Pfad-Konvention: kyc-documents/{contact_id}/{kyc_scan_id}.{ext}
-- kyc-documents/unlinked/{wa_inbound_id}.{ext} (vor Kontakt-Zuweisung)
-- Storage RLS:
-- SELECT: admin, ops (niemals public)
-- INSERT: service_role (Edge Function), admin, ops
-- DELETE: admin only (mit audit_log-Eintrag)
4. Kern-Workflows
4.1 Workflow A — Kamera-Upload in der Caja-App (Feld-Team / Büro)
- Auslöser: Mitarbeitende öffnet Kontakt-Detailansicht oder neuen Kontakt → tippt auf "Ausweis scannen" (Bottom-Sheet öffnet sich).
- Aufnahme: Kamera-Ansicht mit Dokumenten-Rahmen-Overlay (Führungshilfe); Foto aufnehmen oder aus Galerie wählen; HEIC → JPEG-Konversion clientseitig; Qualitätsprüfung (Mindestauflösung 800×500 px, Dateigrösse ≤10 MB).
- Upload: Bild per Supabase Storage SDK →
kyc-documents/unlinked/<uuid>.jpg; Metadaten-Insert inkyc_scans(status=PENDING, source=CAMERA). - OCR-Trigger: Insert in
kyc_scanslöst Supabase Edge Functionfn-kyc-ocraus (viapg_netoder Webhook); Edge Function ruft OCR-Provider auf, schreibtextracted_fields+ocr_confidence+ocr_raw_responsezurück; Status →PENDING(review-bereit). - Review-Screen: Toast "Scan bereit zur Prüfung"; Review-Screen zeigt Bild links / extrahierte Felder rechts (Tablet) oder übereinander (Handy); fehlende/unsichere Felder sind rot markiert (Schwellwert
ocr_confidence_per_field < 0.80); Mitarbeitende korrigiert → schreibtkyc_field_corrections. - Merge in Kontakt: Taste "Bestätigen & in Kontakt übernehmen" → UPSERT in
contacts(Felder nur befüllen wenn bisher leer ODER mit Bestätigungs-Dialog wenn abweichend);kyc_scans.review_status→APPROVED;contacts.kyc_status→VERIFIED; Bild verschoben nachkyc-documents/<contact_id>/<kyc_scan_id>.jpg. - Audit: Jede Statusänderung, Feldkorrektur und Merge-Aktion wird in
audit_logerfasst (Tabelle, Aktion, user_id, Vorher/Nachher als JSONB).
Edge Cases:
- Bild zu dunkel/unscharf → OCR-Confidence global < 0.50 → Status
REJECTEDmit Hinweis "Bitte erneut fotografieren"; Retry-Button. - Dokument bereits in DB (
cedula_numberUnique-Konflikt) → Hinweis "Kontakt existiert bereits" + Verknüpfungs-Option. - Netzwerkausfall während Upload → PWA-Queue hält Bild lokal (IndexedDB), Upload wiederholt sich bei Verbindung.
4.2 Workflow B — WhatsApp-Bild-Eingang
- Auslöser: Kunde sendet Ausweis-Foto an die WhatsApp Business-Nummer (+41 79 199 93 93); Cloud API Webhook → Edge Function
fn-wa-webhook. - Webhook-Verarbeitung: Edge Function validiert
X-Hub-Signature-256(HMAC); liestmessage.type == 'image'; Insert inwhatsapp_inbound(status=RECEIVED); gibt HTTP 200 sofort zurück (WA-Timeout: 5 s). - Medien-Download: Hintergrund-Task (gleiche oder separate Edge Function) lädt Bild von WA-Media-URL herunter (Bearer Token, gültig 24 h); speichert unter
kyc-documents/unlinked/<wa_inbound_id>.jpg; Updatewhatsapp_inbound.storage_path, status=PROCESSING. - Telefonnummer-Matching: Suche nach E.164-normalisierter WA-Nummer gegen
contacts.whatsapp_number, Fallbackcontacts.phone_primary(K-03;contacts.phoneexistiert kanonisch nicht) → falls gefunden:whatsapp_inbound.contact_idsetzen. - OCR: wie Workflow A Schritt 4; Insert in
kyc_scans(source=WHATSAPP,wa_inbound_idgesetzt). - Benachrichtigung an Team: Push-Notification / In-App-Toast "Neuer WA-Ausweis-Scan von +41 79 …" → leitet zu Review-Screen.
- Review & Merge: identisch mit Workflow A Schritt 5–7; nach Merge: optionale automatische WA-Antwort "Ihre Daten wurden erfasst. Danke!" (🔲 zu bestätigen: Auto-Antwort gewünscht?).
Edge Cases:
- Kein Bild in Nachricht (Text, Audio, Sticker) → status=
IGNORED; kein Scan erzeugt. - Mehrere Bilder in einer Nachricht → je Bild ein
whatsapp_inbound-Eintrag, je einkyc_scans-Eintrag. - WA-Media-Download schlägt fehl (URL abgelaufen) → Retry max. 3×, danach status=
IGNORED+ Fehlermeldung; Team-Alert. - Absender-Nummer nicht in
contacts→whatsapp_inbound.contact_id = NULL; Team sieht "Unbekannte Nummer" im Review-Inbox.
4.3 Workflow C — Re-OCR (erneute Analyse)
- Mitarbeitende klickt "Erneut analysieren" auf einem
REJECTED- oderPENDING-Scan. - Edge Function
fn-kyc-ocrruft den self-hosted OCR-Service (Proxmox) erneut auf (z. B. nach Bildkorrektur/2. Seite;ocr_providerbleibtSELF_HOSTED). - Neue
ocr_raw_response+extracted_fieldsüberschreiben den bestehendenkyc_scans-Satz. - Vorherige Werte werden in
kyc_field_correctionsarchiviert (Vorher = alter OCR-Wert, Nachher = neuer OCR-Wert,corrected_by = system).
5. UI-Screens (Mobile/Tablet-First)
Screen 1 — KYC-Inbox (WhatsApp-Eingang-Queue)
Zweck: Übersicht aller eingehenden WA-Bilder und offenen Scan-Reviews.
| Formfaktor | Adaptives Pattern |
|---|---|
| Handy (375px) | Karten-Liste (1 Spalte); pro Karte: Miniaturvorschau (48×48), WA-Nummer, Zeitstempel, Status-Badge; Swipe-Right = Annehmen/Review starten; Swipe-Left = Ignorieren. Bottom-Tab-Bar (Tab "KYC"). |
| Tablet | Zweispaltig: Links Karten-Liste, Rechts Vorschau + Felder-Formular (Split-View). |
| Desktop | Sidebar + Hauptbereich dreispaltig (Liste / Bild / Felder). |
Elemente: Status-Filter-Chips (PENDING / APPROVED / REJECTED), Suche nach Nummer/Name, leerer Zustand "Keine neuen Scans" mit Illustration.
Screen 2 — Scan-Aufnahme (Kamera / Upload)
Zweck: Dokument fotografieren oder aus Galerie laden.
| Formfaktor | Adaptives Pattern |
|---|---|
| Handy | Vollbild-Kamera mit Dokument-Rahmen-Overlay (weisser Rahmen mit Ecken); Shutter-Button am unteren Rand (≥72px, thumb-optimiert); Galerie-Button links, Wechsel Vorder-/Rückkamera rechts. |
| Tablet | Gleich wie Handy (Kamera ist immer Vollbild). |
| Desktop | Datei-Upload-Feld (drag & drop) + Vorschau; keine native Kamera-Aufnahme erzwungen. |
Qualitäts-Feedback in Echtzeit: "Zu dunkel", "Bild verwackelt", "Dokument zu weit entfernt" (via clientseitige Luminanz-Prüfung + Bildschärfe-Heuristik, keine externe API).
Screen 3 — OCR-Review & Merge
Zweck: Extrahierte Felder prüfen, korrigieren, in Kontakt übernehmen.
| Formfaktor | Adaptives Pattern |
|---|---|
| Handy | Vertikales Layout: Bild (scrollbar, pinch-to-zoom); darunter Felder als vertikale Liste mit inline-Inputs; unsichere Felder (confidence < 0.80) mit rotem Ring; "Bestätigen"-Button sticky am unteren Rand (Safe-Area). |
| Tablet | Split: Bild links (50%), Felder rechts (50%); beide scrollbar unabhängig. |
| Desktop | Dreispaltig: Bild / Felder / Kontakt-Vorschau (was wird überschrieben). |
Felder-Reihenfolge: Vorname → Nachname → Dokumentnummer → Geburtsdatum → Nationalität → Adresse DR → Ablaufdatum. Geburtsdatum und Datum-Felder: inputmode="numeric" + Datumspicker. Dokumentnummer: inputmode="text" mit Formatierungsmaske für Cédula (000-0000000-0). Alle Inputs ≥44px Zielhöhe.
Konflikt-Warnung: wenn contacts-Feld bereits befüllt und abweichend → gelbes Alert "Feld bereits vorhanden: [alter Wert]. Überschreiben?" mit Ja/Behalten-Buttons.
Screen 4 — Kontakt-KYC-Status (in Kontakt-Detailansicht)
Zweck: KYC-Übersicht innerhalb des Kontaktsatzes.
| Formfaktor | Adaptives Pattern |
|---|---|
| Handy | Sektion "Identitätsprüfung" im Kontakt-Detail als ausklappbares Accordion (collapsed by default); zeigt kyc_status-Badge + Verifikationsdatum + "Ausweis scannen"-Button. |
| Tablet/Desktop | Sektion immer sichtbar in Kontakt-Sidebar. |
Enthält: Scan-Verlauf (Liste vergangener kyc_scans), jeweils mit Datum, Provider, Status, Link zur Scan-Detailansicht.
6. Integrationen & Verbindungen zu anderen Modulen
Geteilte Entitäten
| Entität | Richtung | Beschreibung |
|---|---|---|
contacts | Schreiben | OCR-Merge befüllt Stammdaten; kyc_status steuert Zoll-/Empfänger-Matching |
audit_log | Schreiben | Alle KYC-Aktionen revisionssicher (Plattform-Modul 8) |
auth.users | Lesen | reviewed_by verweist auf Supabase-Auth (Plattform-Modul 8) |
Events & Auto-Posting
| Event | Auslöser | Empfänger-Modul |
|---|---|---|
kyc.scan.received | WA-Bild eingetroffen | Plattform (Notification an Team) |
kyc.scan.approved | Merge bestätigt | CRM/Modul 1 (Contact-Update), Sendung/Modul 5 (Empfänger-DR freigegeben) |
kyc.scan.rejected | Scan abgelehnt | Plattform (Alert an Mitarbeitende) |
contact.kyc_status → VERIFIED | nach kyc.scan.approved | Offerten-Modul 4 (KYC-Pflichtfeld-Prüfung), Logistik/Modul 5 |
Verbindung zu Modul 5 (Logistik/ERP)
- F-08: KYC-Gate hängt am Empfänger der Sendung, nicht an der Box:
shipments.recipient_id → recipients; verifiziert wird überrecipients.cedula_number(bzw.recipients.contact_id → contacts.kyc_status = VERIFIED, falls ein Golden Record verknüpft ist).boxeshat keine Spalterecipient_contact_id. Cédula-Nummer ist Pflichtfeld für den DR-Zoll. - Sendungsmodul kann
kyc_scan_idreferenzieren zur Dokumentenarchivierung beim Zolldossier.
Verbindung zu Modul 1 (CRM)
- Lead-Intake (Website-Formular) →
contactsmitkyc_status = 'NONE'. - Nach WA-Ausweis-Upload:
kyc_status→'PENDING'→ nach Review'VERIFIED'. - Fuzzy-Match / Golden-Record:
cedula_numberals stärkster Deduplikations-Schlüssel (eindeutig pro Person).
WhatsApp Cloud Business API (Meta) — ab Start (Geschäftsentscheid 2026-06-28, #21)
Entschieden: Die Anbindung erfolgt von Anfang an über die volle offizielle WhatsApp Cloud Business API (Meta) — Direct API über ein eigenes Meta-Business-Konto + WABA (Phone Number ID), kein manueller Deep-Link-MVP und kein BSP-Zwischenschritt. Das deckt beide Richtungen ab:
- Ausgehend (Versand): Der Notification-Worker (
notify-worker, Modul 20 §9.4) sendet automatisch via Meta Graph API (POST /{phone-number-id}/messages) mit vorab genehmigten Message-Templates und erfasstem Opt-in des Empfängers; Erfolg schreibtnotifications.provider_message_id(= WhatsApp-messages[0].id), spätere Delivery-/Read-Status aktualisieren die Zeile per Webhook. - Eingehend (OCR-Intake + Status): Der Cloud-API-Webhook (
whatsapp-inbound/api/webhooks/whatsapp, Modul 20 §3.1/§9.4) nimmt eingehende Nachrichten & Medien (Ausweis-Fotos → Workflow B) sowie Status-Callbacks entgegen; HMAC-Verifikation überX-Hub-Signature-256.
Setup-Anforderungen: eigenes Meta-Business-Konto + WABA + Phone Number ID (Business-Nummer +41 79 199 93 93); Template-Genehmigung durch Meta vor Produktivversand; Opt-in-Erfassung am Kontakt; Secrets WHATSAPP_* (Access-Token, Phone-Number-ID, WABA-ID, Verify-Token, App-Secret) nur server-seitig (Modul 20 §10.2).
Migrationspfad zu einem BSP (z. B. 360dialog, Twilio) bleibt jederzeit möglich (gleiche Cloud-API-Semantik), ist aber nicht der Startweg.
7. Validierungen & Edge-Cases
Eingabe-Validierungen
| Feld | Regel |
|---|---|
| Bild-Datei | MIME: image/jpeg, image/png, image/webp, application/pdf; Grösse ≤ 10 MB; Mindestdimension 800×500 px (clientseitig geprüft) |
cedula_number | Format \d{3}-\d{7}-\d{1} (dominikanische Cédula); serverseitige Regex-Prüfung vor Insert in contacts |
passport_number | keine starre Formatregel (international variabel); Minimum 5 Zeichen, alphanumerisch |
birth_date | Plausibilität: nicht in Zukunft, Alter 0–120 Jahre |
expiry_date | nicht in Vergangenheit (Warnung wenn abgelaufen, kein harter Fehler) |
ocr_confidence | Range 0.000–1.000; Werte ausserhalb → Fehler in Edge Function, Fallback null |
Wichtige Edge-Cases
- Doppelter Scan desselben Dokuments:
UNIQUE INDEXaufcontacts.cedula_numberverhindert Duplikat; Dialog "Dieser Ausweis ist bereits dem Kontakt [Name] zugeordnet. Verknüpfen oder neuen Kontakt anlegen?" - Falsches Dokument: OCR erkennt kein Identitätsdokument →
extracted_fields = {},ocr_confidence < 0.30→ automatischreview_status = REJECTEDmit Grund "Kein Identitätsdokument erkannt". - Zweisprachige Cédula (Vorder-/Rückseite nötig): OCR-Provider erhält beide Bilder; Caja-App ermöglicht Upload von 2 Bildern pro Scan (
raw_image_pathkann JSONB-Array sein:["...front.jpg","...back.jpg"]). 🔲 Zu bestätigen: Reicht Vorderseite für Zoll-Anforderungen? - Netzwerkunterbrechung (Offline-Toleranz): Bild + Metadaten in IndexedDB zwischengespeichert; Service Worker sendet bei Wiederverbindung; UI zeigt "Wird synchronisiert…"-Indicator.
- OCR-Provider-Ausfall: Retry-Logik (max. 3 Versuche mit Exponential Backoff); bei persistentem Fehler →
kyc_scans.review_status = 'PENDING',ocr_provider = 'MANUAL'; Team-Alert "OCR nicht verfügbar — manuelle Eingabe erforderlich". - WA-Webhook-Duplikat: WhatsApp Cloud API sendet bei Timeout erneut;
wa_message_id UNIQUE-Constraint verhindert Doppelverarbeitung. - Grosse Bilddatei (>10 MB): clientseitige Kompression auf ≤ 2 MP / ≤ 2 MB vor Upload (Canvas-Resizing); Qualitätsverlust akzeptabel für OCR-Zweck.
8. Compliance- / Sicherheits-Hinweise
revDSG (revidiertes Datenschutzgesetz Schweiz, in Kraft seit 01.09.2023)
Ausweisdaten (Name, Geburtsdatum, Dokumentnummer, Adresse, Lichtbild) gelten als besonders schützenswerte Personendaten im Sinne von Art. 5 lit. c revDSG:
| Pflicht | Umsetzung in Caja |
|---|---|
| Zweckbindung | Ausweisdaten ausschliesslich für KYC (Zoll-/Empfänger-Matching DR) und regulatorische Pflichten; in kyc_scans.notes dokumentiert; keine Weitergabe an Dritte ausser DR-Zollbehörden |
| Datensparsamkeit | OCR extrahiert nur die für den Geschäftszweck notwendigen Felder; ocr_raw_response (vollständige Provider-Antwort) wird nach 90 Tagen automatisch auf NULL gesetzt (CRON-Job), Felder bleiben |
| Speicherbegrenzung | Rohdaten-Bilder in kyc-documents und extrahierte Felder: max. 24 Monate nach der letzten Interaktion mit dem Kontakt, danach Löschung (Geschäftsentscheid 2026-06-28, löst OE-11/F-21 — kein Rechtsgutachten zur Frist mehr nötig). retain_until = last_interaction_at + 24 M (rollierend); Lifecycle-Job löscht nach Fristablauf, audit_log-Eintrag der Löschung bleibt. Buchführungsrelevante KYC-Textfelder (Identität Vertragspartei als Beleg) folgen separat OR 958f (10 J, Compliance §1.4). Details: Compliance §4.6 |
| Datensicherheit | Supabase Storage Bucket kyc-documents ist privat (kein öffentlicher URL); Pre-Signed URLs mit max. 60 Minuten Gültigkeit für Review-Screen; RLS verhindert unbefugten Datenbankzugriff; Verschlüsselung at-rest via Supabase (AES-256) und in-transit (TLS 1.2+) |
| Betroffenenrechte | Auskunft/Löschung: admin kann kyc_scans und Storage-Datei löschen; Löschung wird in audit_log mit Grund erfasst; Löschung von contacts.cedula_number / birth_date separat möglich ohne Kontakt zu löschen |
| Informationspflicht | Datenschutzerklärung auf Website und In-App-KYC-Hinweis nennen KYC-Verarbeitung, Zweck, Empfänger und Auslandtransfer. Entwurf liegt vor: 46-datenschutz.md (revDSG); 🔲 verbleibend anwaltliche Schlussprüfung + Publikation |
| Einwilligung (Onboarding-Vertrag) | Die OCR-Verarbeitung läuft self-hosted (Proxmox), ohne Drittland-Transfer fürs OCR (Entscheid 2026-06-28 Teil 3); einwilligungsbedürftig bleibt der Auslandtransfer der extrahierten Ausweisdaten in die DR (Zoll/Empfänger-Matching). Stützung auf die Einwilligung im Onboarding-Vertrag (Art. 6 Abs. 6 / Art. 17 revDSG). Erfassung über contacts.consent_onboarding_at (siehe Einwilligungs-Gate unten); ohne erfasste Einwilligung kein KYC-Processing/DR-Transfer |
| Auftragsverarbeitung | OCR ist in-house (self-hosted auf eigener Proxmox-Infra) → kein externer OCR-Auftragsbearbeiter, kein OCR-DPA und kein Drittland-Transfer fürs OCR. Auftragsbearbeitungsverträge betreffen nur die übrigen Provider (Supabase, Vercel, Resend, WhatsApp/Meta); benannt in 46-datenschutz.md §7 |
Einwilligungs-Gate (Onboarding-Vertrag) — Voraussetzung für KYC-Verarbeitung + Auslandtransfer
Das OCR selbst läuft self-hosted (Proxmox) ohne Drittland-Transfer (Entscheid 2026-06-28 Teil 3) — eine Übermittlung des Ausweisbildes an einen externen OCR-Dienstleister findet nicht statt. Einwilligungsbedürftig bleibt damit primär die Bekanntgabe der extrahierten Ausweisdaten in die Dominikanische Republik (Zoll/Empfänger-Matching, Drittland ohne Angemessenheit) sowie generell die Bearbeitung der besonders schützenswerten Ausweisdaten. Beides stützt sich auf die Einwilligung, die der Kunde im Onboarding-Vertrag erteilt (Compliance §4.4.1; 46-datenschutz.md §4 + §6).
Erfassung der Einwilligung:
- Der Onboarding-Flow erfasst die Einwilligung explizit und referenziert den Onboarding-Vertrag. Vorgeschlagene Feld-Referenz am Kontakt:
contacts.consent_onboarding_at timestamptz(Zeitpunkt der erteilten Onboarding-Einwilligung;NULL= keine Einwilligung erfasst). Optional ergänzbar:consent_onboarding_ref text(Vertrags-/Dokumentreferenz, z. B.storage_objects-Bezug zum unterzeichneten Onboarding-Vertrag).Schema-Hinweis: Die kanonische DDL liegt in Dok 30 (
30-schema-konsolidiert.md,contacts-Erweiterung) und ist dort als 🔲 nachzuziehen — dieses Modul nennt das Feld nur als fachliche Anforderung, definiert es aber nicht normativ. - Alternativ/ergänzend kann die Einwilligung am Scan referenziert werden (
kyc_scans— Bezug auf den einwilligenden Kontakt), damit jeder Scan einen nachvollziehbaren Einwilligungsbezug trägt.
Gate-Regel (hart):
- Ohne erfasste Einwilligung (
contacts.consent_onboarding_at IS NULL) kein KYC-Processing. Die Edge Functionfn-kyc-ocrprüft den Einwilligungsbezug, bevor das Bild an den self-hosted OCR-Service (Proxmox) übergeben und/oder die extrahierten Daten für den DR-Transfer freigegeben werden; fehlt er, wird der Scan aufreview_status='PENDING'mit Hinweis „Einwilligung (Onboarding) fehlt" gehalten. (Auch wenn das OCR in-house läuft, bleibt das Einwilligungs-Gate für die Bearbeitung besonders schützenswerter Daten + DR-Transfer bestehen.) - Beim WhatsApp-Eingang (Workflow B) eines Ausweisbildes von einer Nummer ohne erfasste Onboarding-Einwilligung: Bild wird im privaten
kyc-Bucket abgelegt, aber kein OCR ausgelöst; das Team holt die Onboarding-Einwilligung nach, bevor verarbeitet wird. - Der Widerruf der Einwilligung (Datenschutzerklärung §9) stoppt künftige Verarbeitung; bereits erfasste Daten unterliegen der 24-Monats-Frist (oben) bzw. der OR-Frist, soweit Buchungsbeleg.
OR 957 / GeBüV (Revisionssicherheit)
kyc_field_correctionsundaudit_logsind append-only (kein UPDATE/DELETE auf bestehende Einträge füradmin; service_role-only für technische Korrekturen).- Buchhaltungsrelevante KYC-Daten (Identifikation der Vertragspartei, soweit zwingender Buchungsbeleg) unterliegen der 10-Jahres-Aufbewahrungspflicht (OR 958f). Das Ausweis-Lichtbild und die KYC-Detailfelder sind hingegen kein zwingender Buchungsbeleg und folgen der 24-Monats-Frist (max. 24 M nach letzter Interaktion, Entscheid 2026-06-28; Compliance §4.6). Zwei getrennte Schichten, zwei getrennte Fristen.
OCR-Provider-Datenschutz — ENTSCHIEDEN: self-hosted (Proxmox)
✅ Entscheid 2026-06-28 (Teil 3): OCR läuft self-hosted auf eigener Proxmox-Infra (ocr_provider='SELF_HOSTED'). Die Ausweisbilder verlassen die eigene Infrastruktur fürs OCR nicht — kein Cloud-/Drittland-Transfer der Ausweisdaten für die Texterkennung. Das ist ein Compliance-Plus (Datenminimierung/Verhältnismässigkeit, kein OCR-Auftragsbearbeiter, kein OCR-DPA). Die früher diskutierten Cloud-Provider (Anthropic / Google Vision / Azure) werden nicht eingesetzt.
✅ Modell-Stack ENTSCHIEDEN (2026-06-28, Best-Practice-Default — final bestätigbar): primär ein Vision-LLM, fallback ein spezialisierter Document-Parser. Beide Apache-2.0 (kommerzielle Nutzung erlaubt), Gewichte lokal, kein externer Call.
- Primär — Qwen3-VL-8B-Instruct (Apache 2.0). Vision-LLM (Okt 2025), extrahiert in einem Schritt direkt strukturierte Felder aus dem Ausweisfoto (Key-Information-Extraction), nicht nur rohen Text. OCR ist auf 32 Sprachen erweitert (Spanisch + Deutsch enthalten), robust bei schlechtem Licht / Unschärfe / schräger Aufnahme — genau das Profil von Handy-/WhatsApp-Fotos einer Cédula. Über vLLM mit JSON-Schema-erzwungener Ausgabe (guided/structured decoding) liefert das Modell garantiert valides JSON in der
extracted_fields-Struktur; per-Feld-Confidence wird aus den Token-Logprobs abgeleitet. DocVQA ~96 % (8B). Lizenz: Apache 2.0 für die gesamte Qwen3-VL-Reihe (anders als die alte Qwen2.5-VL-72B-„Qwen License" — entfällt damit). - Fallback / Vorstufe — PaddleOCR-VL (Apache 2.0). Ultra-kompakter 0.9B-Document-Parser (Baidu, Jan-2026-Linie 1.5/1.6), SOTA auf OmniDocBench v1.5/1.6 (94.5 % → 96.3 %, schlägt GPT-4o und Qwen2.5-VL-72B beim reinen Document-Parsing), 109 Sprachen (inkl. Spanisch), VRAM ~3–4 GB optimiert. Einsatz: (a) wenn das Vision-LLM ein Feld unsicher liest → Plain-OCR-Text als zweite Quelle / Re-OCR (Workflow C); (b) als Notfall-Engine, falls die Qwen-Instanz nicht verfügbar ist. Liefert Text + Layout (JSON/Markdown); die Feld-Zuordnung erfolgt dann durch einen kleinen lokalen LLM-Nachverarbeitungsschritt.
- Begründung der Reihenfolge: Ein Vision-LLM (Qwen3-VL) ist der 2026er Best-Practice-Default für strukturierte ID-Extraktion, weil es Felder + Plausibilität in einem Schritt liefert (kein separates Layout→Regex-Mapping nötig) und mehrsprachig + robust gegen reale Fotoqualität ist. Klassische OCR allein (Tesseract/EasyOCR, ~80 % MRZ-Präzision) bräuchte zusätzliche Feld-Logik und ist auf Cédula-Layout weniger zuverlässig; sie bleibt darum nur als Fallback/Zweitquelle. PaddleOCR-VL ist die genaueste reine Parsing-Engine und ergänzt das Vision-LLM ideal, ohne dessen Halluzinations-Risiko bei Plain-Text.
Confidence-Routing (Best Practice 2026):
ocr_confidence ≥ 0.90→ Felder direkt vorausgefüllt;0.70–0.90→ Feld rot markiert, Plausibilitäts-/Regex-Check + optional PaddleOCR-VL-Zweitlesung;< 0.70(bzw. global< 0.50) → Scan aufPENDING/REJECTEDfür manuellen Review. Die Confidence stammt aus den Token-Logprobs des Vision-LLM (gut kalibriert für die kurzen, strukturierten Feldwerte einer Cédula); zusätzlich harte Format-Validierung (cedula_number-Regex, Datums-Plausibilität, §7). Kein Auto-Approve ohne menschlichen Review — das Vision-LLM füllt nur vor.
| Option | Datenweitergabe | Lizenz (kommerziell?) | AV-Vertrag | Status |
|---|---|---|---|---|
| Qwen3-VL-8B-Instruct (self-hosted, Proxmox) | kein Transfer — Bild bleibt auf eigener Infra | Apache 2.0 — ja | nicht nötig | ✅ primär gewählt |
| PaddleOCR-VL 0.9B (self-hosted, Proxmox) | kein Transfer — eigene Infra | Apache 2.0 — ja | nicht nötig | ✅ Fallback / Zweitquelle |
| Qwen3-VL-4B-Instruct (kleinere GPU) | kein Transfer | Apache 2.0 — ja | nicht nötig | ↺ Sparvariante (≥6 GB VRAM) |
| Tesseract / EasyOCR / docTR | kein Transfer | Apache 2.0 / MIT — ja | nicht nötig | ↺ nur als Notfall-OCR |
| Anthropic Claude Vision | Daten verlassen EU/CH (US) | proprietär (API) | DPA nötig | ✗ nicht genutzt |
Google Cloud Vision (europe-west6 Zürich) | Cloud (EU-Region) | proprietär (API) | DPA nötig | ✗ nicht genutzt |
| Azure AI Document Intelligence (CH North) | Cloud (CH-Region) | proprietär (API) | DPA nötig | ✗ nicht genutzt |
KYC-Zugriffskontrolle
- Rohdaten-Bilder und
extracted_fieldssind via RLS nur füradminundopslesbar. driver(Arkys) kann Scans einreichen, aber keine Daten einsehen oder ändern nach Upload.- Alle Zugriffe auf
kyc_scansundkyc-documentswerden inaudit_logprotokolliert (via Supabase Trigger oder Edge Function).
9. Deployment auf Proxmox (self-hosted OCR-Service)
9.1 Architektur
Caja (Supabase Edge Function fn-kyc-ocr)
│ HTTPS POST (intern, nur im eigenen Netz/VPN)
▼
OCR_SELFHOST_URL → Reverse-Proxy (Caddy/Nginx, TLS + Bearer-Token)
│
├── Service A: vLLM ─ Qwen3-VL-8B-Instruct (primär, GPU)
└── Service B: PaddleOCR-VL (Fallback / Zweitlesung)
Der OCR-Service ist ein eigener interner Dienst auf der Proxmox-Infra; er ist nicht öffentlich erreichbar (nur erreichbar für die Caja-Backend-Komponente, z. B. via privates Netz / WireGuard / Tailscale). Das Bild wird nur an diesen internen Endpoint geschickt und verlässt die eigene Infra nicht.
9.2 Container vs. VM — Empfehlung: LXC mit GPU-Passthrough
| Kriterium | LXC (empfohlen) | VM |
|---|---|---|
| GPU-Sharing | GPU bleibt teilbar mit anderen Diensten (Frigate, Jellyfin, Immich …) | GPU exklusiv für die VM gebunden |
| Overhead | minimal (kein zweiter Kernel) | höher |
| Setup | NVIDIA-Treiber nur auf dem Proxmox-Host, Devices per dev0…devN in die LXC durchreichen (Proxmox 8.1+ dev*-Syntax, gid=44); Treiber nicht im Container installieren | klassisches PCIe-Passthrough (VFIO), Treiber in der VM |
| Empfehlung | ✅ Default — beste Auslastung bei Einzel-GPU | nur wenn strikte Isolation gefordert |
Best-Practice-Hinweise: NVIDIA Persistence-Mode als systemd-Service aktivieren (Cold-Start von ~3 s auf <100 ms); vLLM und PaddleOCR-VL je als Docker-Container innerhalb der LXC (oder zwei LXC); Modell-Gewichte auf lokalem schnellem Storage cachen (HF_HOME), kein automatischer Download-zur-Laufzeit aus dem Internet in der Produktion (Gewichte einmalig vorab ziehen, dann offline).
9.3 Ressourcenbedarf (Richtwerte)
| Komponente | VRAM | RAM | Hinweis |
|---|---|---|---|
| Qwen3-VL-8B-Instruct (primär) | ~16 GB (BF16/FP16 komfortabel; ~8 GB minimal mit AWQ/FP8-Quant) | 16–32 GB | 1× GPU RTX 4080/4090/A4000-Klasse, Compute Capability ≥ 8.0 reicht für das erwartete Volumen (wenige Scans/Tag, kein Dauer-Batch) |
| Qwen3-VL-4B (Sparvariante) | ~6–8 GB | 16 GB | falls nur eine kleine GPU vorhanden (RTX 3060/4060); leicht tiefere Genauigkeit |
| PaddleOCR-VL 0.9B (Fallback) | ~3–4 GB (optimiert) | 8 GB | sehr leichtgewichtig; läuft notfalls auch CPU-only (langsamer) |
CPU-only-Fallback: PaddleOCR-VL ist klein genug, um notfalls ohne GPU zu laufen (Re-OCR/Notbetrieb). Das Vision-LLM (Qwen3-VL) braucht praktisch eine GPU für brauchbare Latenz — für den Regelbetrieb daher 1× GPU einplanen.
9.4 Betrieb & Skalierung
- Niedriges Volumen (KYC ist kein Massendurchsatz) → eine GPU-Instanz genügt; vLLM serialisiert/batcht Anfragen.
- Queue + Retry wie in §4/§7 (Re-OCR-Last, OP #8): Edge Function ruft den Endpoint mit Timeout + Exponential Backoff (max. 3×); bei Ausfall →
ocr_provider='MANUAL', Team-Alert. - Monitoring: GPU-Auslastung, Latenz p95, Fehlerrate; Healthcheck-Endpoint
/healtham Reverse-Proxy.
10. Interner API-Kontrakt (OCR_SELFHOST_URL)
Caja (fn-kyc-ocr) ruft einen internen Endpoint auf, schickt das Bild (oder eine kurzlebige Storage-Pre-Signed-URL innerhalb der eigenen Infra) und bekommt strukturierte Felder + Confidence zurück. Kein externer Call, kein Drittland. Secret OCR_SELFHOST_URL + OCR_SELFHOST_TOKEN nur server-seitig (analog Modul 20 §10.2).
10.1 Request
POST {OCR_SELFHOST_URL}/v1/kyc/extract
Authorization: Bearer {OCR_SELFHOST_TOKEN}
Content-Type: application/json
{
"document_type": "CEDULA", // CEDULA | PASSPORT | OTHER (Hinweis fürs Prompt-Schema)
"images": [ // 1..2 Bilder (Vorder-/Rückseite), base64 ODER interne URL
{ "b64": "<base64-jpeg>" }
],
"lang_hint": ["es", "de"], // bevorzugte Sprachen
"want_confidence": true
}
10.2 Response (deckt sich 1:1 mit kyc_scans.extracted_fields)
{
"ocr_provider": "SELF_HOSTED",
"model": "qwen3-vl-8b-instruct", // bzw. "paddleocr-vl" im Fallback
"document_type": "CEDULA",
"extracted_fields": {
"first_name": "Juan",
"last_name": "García Pérez",
"doc_number": "001-1234567-8",
"birth_date": "1985-03-22",
"nationality": "DO",
"address": "Calle 5 No. 10, Santo Domingo",
"expiry_date": "2028-03-22",
"gender": "M"
},
"confidence_per_field": {
"first_name": 0.97, "last_name": 0.95, "doc_number": 0.91,
"birth_date": 0.93, "expiry_date": 0.88
},
"ocr_confidence": 0.93, // aggregiert (z. B. min/Mittel der Feld-Confidences)
"raw": { "...": "vollständige Modellantwort → kyc_scans.ocr_raw_response" },
"warnings": ["expiry_date low confidence"]
}
Vertragsregeln:
- Antwort ist immer valides JSON gegen ein festes JSON-Schema (vLLM guided/structured decoding erzwingt das); unsichere Felder kommen als
null+ niedrige Confidence statt halluziniert. ocr_confidenceundconfidence_per_fieldfüllen direkt die Schema-Spalten; das Confidence-Routing (§8) entscheidet Auto-Vorfüllen vs. Review.- Bei
model=paddleocr-vl(Fallback) liefert der Service Plain-Text/Layout, mappt aber serverseitig auf dieselbeextracted_fields-Form (kleiner lokaler LLM-Nachschritt), sodass der Kontrakt stabil bleibt —ocr_providerbleibtSELF_HOSTED. - Implementierungs-Detail: vLLM stellt eine OpenAI-kompatible Vision-API bereit (
/v1/chat/completionsmit Bild-Input +response_format/guided_json);/v1/kyc/extractist der dünne, Caja-spezifische Wrapper davor (kapselt Prompt + Schema + Fallback-Routing).
11. Offene Punkte
| # | Punkt | Priorität |
|---|---|---|
| ✅ 1 | OCR-Provider + Modell-Stack ENTSCHIEDEN (2026-06-28, Teil 3): self-hosted auf eigener Proxmox-Infra (ocr_provider='SELF_HOSTED') — kein Cloud-/Drittland-Transfer der Ausweisbilder fürs OCR (Compliance-Plus, kein OCR-DPA). Stack: Qwen3-VL-8B-Instruct (Apache 2.0) primär für strukturierte Feld-Extraktion + Confidence via vLLM/JSON-Schema, PaddleOCR-VL (Apache 2.0) als Fallback/Zweitlesung; Details + Deployment + API-Kontrakt s. §8–§10. Cloud-Provider (Anthropic/Google/Azure) werden nicht eingesetzt. (Best-Practice-Default, final bestätigbar.) | erledigt |
| ✅ 2 | WhatsApp-Anbindung ENTSCHIEDEN (2026-06-28, #21): volle offizielle WhatsApp Cloud Business API (Meta), Direct API (eigenes WABA), ab Start — automatischer Versand inkl. provider_message_id, genehmigte Templates, Opt-in, Webhooks für ein-/ausgehende Nachrichten. Kein Deep-Link-MVP, kein BSP-Start (Migrationspfad bleibt offen). Setup/Secrets s. §6 + Modul 20 §9.4/§10.2. | erledigt |
| 🔲 3 | Auto-Antwort nach WA-Scan: Soll Caja automatisch via WhatsApp bestätigen "Ihre Daten wurden erfasst. Danke!"? Wenn ja: in welcher Sprache (ES/DE/EN je nach Kundenpräferenz)? | Mittel |
| 🔲 4 | Cédula Vorder-/Rückseite: Reicht die Vorderseite für Zoll-Anforderungen DR? Oder müssen beide Seiten erfasst werden? Beeinflusst OCR-Flow und UI. | Hoch |
| 🟡 5 | Datenschutzerklärung: Entwurf liegt vor (46-datenschutz.md, revDSG-konform inkl. KYC, Auftragsbearbeiter, Auslandtransfer). Verbleibend: anwaltliche Schlussprüfung (revDSG-Spezialist), Platzhalter füllen, Publikation /datenschutz + In-App-Hinweis verlinken. | Mittel |
| ✅ 6 | Aufbewahrungsfristen ENTSCHIEDEN (2026-06-28, löst OE-11/F-21): KYC-Rohbild + extrahierte Felder = max. 24 Monate nach letzter Interaktion (retain_until = last_interaction_at + 24 M, rollierend); danach Löschung. Buchführungsrelevante KYC-Textfelder (Identität Vertragspartei als Beleg) folgen weiterhin OR 958f (10 J). Rechtsgrundlage = Onboarding-Vertrag (Einwilligung) + Vertragserfüllung. Kein Rechtsgutachten zur Frist mehr nötig. Konsistent: Compliance §4.6, Design §8.1. | erledigt |
| 🔲 7 | Offline-OCR-Scope: Soll die PWA auch ohne Netz eine Offline-Qualitätsprüfung (Luminanz/Schärfe) lokal durchführen, oder genügt einfaches Queuing? | Niedrig |
| 🔲 8 | Re-OCR-Last (self-hosted): Keine Provider-Kosten mehr (in-house, Entscheid #1). Stattdessen Kapazität/Last der Proxmox-OCR-Instanz dimensionieren (Durchsatz, Queue, Monitoring) für Spitzen bei vielen Scans/Re-OCR. | Niedrig |