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änzt
  • kyc_scans — ein Scan-Versuch; enthält Rohdaten + extrahierte Felder + Review-Status
  • whatsapp_inbound — Eingangs-Queue für WhatsApp-Nachrichten (Medien + Metadaten)
  • kyc_field_corrections — Audit-Journal für manuelle Korrekturen an extrahierten Feldern
  • audit_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_types folgt (wie alle Lookups) der zentralen RLS-Policy in Dok 30 §13 (CRU für ADMIN, R fü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, UPDATE
  • driver (Arkys): kein Zugriff
  • affiliate: 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 Zugriff
  • readonly: 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)

  1. Auslöser: Mitarbeitende öffnet Kontakt-Detailansicht oder neuen Kontakt → tippt auf "Ausweis scannen" (Bottom-Sheet öffnet sich).
  2. 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).
  3. Upload: Bild per Supabase Storage SDK → kyc-documents/unlinked/<uuid>.jpg; Metadaten-Insert in kyc_scans (status=PENDING, source=CAMERA).
  4. OCR-Trigger: Insert in kyc_scans löst Supabase Edge Function fn-kyc-ocr aus (via pg_net oder Webhook); Edge Function ruft OCR-Provider auf, schreibt extracted_fields + ocr_confidence + ocr_raw_response zurück; Status → PENDING (review-bereit).
  5. 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 → schreibt kyc_field_corrections.
  6. 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_statusAPPROVED; contacts.kyc_statusVERIFIED; Bild verschoben nach kyc-documents/<contact_id>/<kyc_scan_id>.jpg.
  7. Audit: Jede Statusänderung, Feldkorrektur und Merge-Aktion wird in audit_log erfasst (Tabelle, Aktion, user_id, Vorher/Nachher als JSONB).

Edge Cases:

  • Bild zu dunkel/unscharf → OCR-Confidence global < 0.50 → Status REJECTED mit Hinweis "Bitte erneut fotografieren"; Retry-Button.
  • Dokument bereits in DB (cedula_number Unique-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

  1. Auslöser: Kunde sendet Ausweis-Foto an die WhatsApp Business-Nummer (+41 79 199 93 93); Cloud API Webhook → Edge Function fn-wa-webhook.
  2. Webhook-Verarbeitung: Edge Function validiert X-Hub-Signature-256 (HMAC); liest message.type == 'image'; Insert in whatsapp_inbound (status=RECEIVED); gibt HTTP 200 sofort zurück (WA-Timeout: 5 s).
  3. 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; Update whatsapp_inbound.storage_path, status=PROCESSING.
  4. Telefonnummer-Matching: Suche nach E.164-normalisierter WA-Nummer gegen contacts.whatsapp_number, Fallback contacts.phone_primary (K-03; contacts.phone existiert kanonisch nicht) → falls gefunden: whatsapp_inbound.contact_id setzen.
  5. OCR: wie Workflow A Schritt 4; Insert in kyc_scans (source=WHATSAPP, wa_inbound_id gesetzt).
  6. Benachrichtigung an Team: Push-Notification / In-App-Toast "Neuer WA-Ausweis-Scan von +41 79 …" → leitet zu Review-Screen.
  7. 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 ein kyc_scans-Eintrag.
  • WA-Media-Download schlägt fehl (URL abgelaufen) → Retry max. 3×, danach status=IGNORED + Fehlermeldung; Team-Alert.
  • Absender-Nummer nicht in contactswhatsapp_inbound.contact_id = NULL; Team sieht "Unbekannte Nummer" im Review-Inbox.

4.3 Workflow C — Re-OCR (erneute Analyse)

  1. Mitarbeitende klickt "Erneut analysieren" auf einem REJECTED- oder PENDING-Scan.
  2. Edge Function fn-kyc-ocr ruft den self-hosted OCR-Service (Proxmox) erneut auf (z. B. nach Bildkorrektur/2. Seite; ocr_provider bleibt SELF_HOSTED).
  3. Neue ocr_raw_response + extracted_fields überschreiben den bestehenden kyc_scans-Satz.
  4. Vorherige Werte werden in kyc_field_corrections archiviert (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.

FormfaktorAdaptives 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").
TabletZweispaltig: Links Karten-Liste, Rechts Vorschau + Felder-Formular (Split-View).
DesktopSidebar + 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.

FormfaktorAdaptives Pattern
HandyVollbild-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.
TabletGleich wie Handy (Kamera ist immer Vollbild).
DesktopDatei-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.

FormfaktorAdaptives Pattern
HandyVertikales 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).
TabletSplit: Bild links (50%), Felder rechts (50%); beide scrollbar unabhängig.
DesktopDreispaltig: 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.

FormfaktorAdaptives Pattern
HandySektion "Identitätsprüfung" im Kontakt-Detail als ausklappbares Accordion (collapsed by default); zeigt kyc_status-Badge + Verifikationsdatum + "Ausweis scannen"-Button.
Tablet/DesktopSektion 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ätRichtungBeschreibung
contactsSchreibenOCR-Merge befüllt Stammdaten; kyc_status steuert Zoll-/Empfänger-Matching
audit_logSchreibenAlle KYC-Aktionen revisionssicher (Plattform-Modul 8)
auth.usersLesenreviewed_by verweist auf Supabase-Auth (Plattform-Modul 8)

Events & Auto-Posting

EventAuslöserEmpfänger-Modul
kyc.scan.receivedWA-Bild eingetroffenPlattform (Notification an Team)
kyc.scan.approvedMerge bestätigtCRM/Modul 1 (Contact-Update), Sendung/Modul 5 (Empfänger-DR freigegeben)
kyc.scan.rejectedScan abgelehntPlattform (Alert an Mitarbeitende)
contact.kyc_status → VERIFIEDnach kyc.scan.approvedOfferten-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 über recipients.cedula_number (bzw. recipients.contact_id → contacts.kyc_status = VERIFIED, falls ein Golden Record verknüpft ist). boxes hat keine Spalte recipient_contact_id. Cédula-Nummer ist Pflichtfeld für den DR-Zoll.
  • Sendungsmodul kann kyc_scan_id referenzieren zur Dokumentenarchivierung beim Zolldossier.

Verbindung zu Modul 1 (CRM)

  • Lead-Intake (Website-Formular) → contacts mit kyc_status = 'NONE'.
  • Nach WA-Ausweis-Upload: kyc_status'PENDING' → nach Review 'VERIFIED'.
  • Fuzzy-Match / Golden-Record: cedula_number als 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 schreibt notifications.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 über X-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

FeldRegel
Bild-DateiMIME: image/jpeg, image/png, image/webp, application/pdf; Grösse ≤ 10 MB; Mindestdimension 800×500 px (clientseitig geprüft)
cedula_numberFormat \d{3}-\d{7}-\d{1} (dominikanische Cédula); serverseitige Regex-Prüfung vor Insert in contacts
passport_numberkeine starre Formatregel (international variabel); Minimum 5 Zeichen, alphanumerisch
birth_datePlausibilität: nicht in Zukunft, Alter 0–120 Jahre
expiry_datenicht in Vergangenheit (Warnung wenn abgelaufen, kein harter Fehler)
ocr_confidenceRange 0.000–1.000; Werte ausserhalb → Fehler in Edge Function, Fallback null

Wichtige Edge-Cases

  • Doppelter Scan desselben Dokuments: UNIQUE INDEX auf contacts.cedula_number verhindert 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 → automatisch review_status = REJECTED mit 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_path kann 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:

PflichtUmsetzung in Caja
ZweckbindungAusweisdaten 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
DatensparsamkeitOCR 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
SpeicherbegrenzungRohdaten-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
DatensicherheitSupabase 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+)
BetroffenenrechteAuskunft/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
InformationspflichtDatenschutzerklä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
AuftragsverarbeitungOCR 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 Function fn-kyc-ocr prü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 auf review_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_corrections und audit_log sind append-only (kein UPDATE/DELETE auf bestehende Einträge für admin; 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 auf PENDING/REJECTED fü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.

OptionDatenweitergabeLizenz (kommerziell?)AV-VertragStatus
Qwen3-VL-8B-Instruct (self-hosted, Proxmox)kein Transfer — Bild bleibt auf eigener InfraApache 2.0 — janicht nötigprimär gewählt
PaddleOCR-VL 0.9B (self-hosted, Proxmox)kein Transfer — eigene InfraApache 2.0 — janicht nötigFallback / Zweitquelle
Qwen3-VL-4B-Instruct (kleinere GPU)kein TransferApache 2.0 — janicht nötig↺ Sparvariante (≥6 GB VRAM)
Tesseract / EasyOCR / docTRkein TransferApache 2.0 / MIT — janicht nötig↺ nur als Notfall-OCR
Anthropic Claude VisionDaten 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_fields sind via RLS nur für admin und ops lesbar.
  • driver (Arkys) kann Scans einreichen, aber keine Daten einsehen oder ändern nach Upload.
  • Alle Zugriffe auf kyc_scans und kyc-documents werden in audit_log protokolliert (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

KriteriumLXC (empfohlen)VM
GPU-SharingGPU bleibt teilbar mit anderen Diensten (Frigate, Jellyfin, Immich …)GPU exklusiv für die VM gebunden
Overheadminimal (kein zweiter Kernel)höher
SetupNVIDIA-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 installierenklassisches PCIe-Passthrough (VFIO), Treiber in der VM
EmpfehlungDefault — beste Auslastung bei Einzel-GPUnur 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)

KomponenteVRAMRAMHinweis
Qwen3-VL-8B-Instruct (primär)~16 GB (BF16/FP16 komfortabel; ~8 GB minimal mit AWQ/FP8-Quant)16–32 GB1× 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 GB16 GBfalls nur eine kleine GPU vorhanden (RTX 3060/4060); leicht tiefere Genauigkeit
PaddleOCR-VL 0.9B (Fallback)~3–4 GB (optimiert)8 GBsehr 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 /health am 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_confidence und confidence_per_field fü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 dieselbe extracted_fields-Form (kleiner lokaler LLM-Nachschritt), sodass der Kontrakt stabil bleibt — ocr_provider bleibt SELF_HOSTED.
  • Implementierungs-Detail: vLLM stellt eine OpenAI-kompatible Vision-API bereit (/v1/chat/completions mit Bild-Input + response_format/guided_json); /v1/kyc/extract ist der dünne, Caja-spezifische Wrapper davor (kapselt Prompt + Schema + Fallback-Routing).

11. Offene Punkte

#PunktPriorität
✅ 1OCR-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
✅ 2WhatsApp-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
🔲 3Auto-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
🔲 4Cé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
🟡 5Datenschutzerklä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
✅ 6Aufbewahrungsfristen 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
🔲 7Offline-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
🔲 8Re-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