Offene Punkte — Completeness-Critic (Lücken, Widersprüche, fehlende Verbindungen)

Querschnitts-Dokument, kein Fachmodul. Diese Datei ist das Ergebnis eines modulübergreifenden Vollständigkeits-Reviews aller Spec-Dokumente (10–18, 20, 30, 40, 45). Sie listet technische Mängel auf: Entitäten, die referenziert aber nirgends definiert sind; Schema-/Code-Widersprüche, die die Konsolidierung (Dok 30, K-01..K-21) nicht aufgelöst hat; unklare oder doppelte Auto-Posting-Ketten; fehlende RLS; sowie fehlende Mobile-Patterns.

Wichtig zur Lesart: Dok 30 (Kanonisches Schema) hat bereits viele naive Modul-Konflikte über die K-Entscheide aufgelöst und gewinnt bei Schema-Abweichungen. Die meisten Befunde hier sind deshalb entweder (a) Residual-Widersprüche, wo ein Modul-Spec nicht auf Dok 30 nachgezogen wurde (= Implementierungsfalle, falls jemand den Modul-Spec statt Dok 30 liest), oder (b) echte Lücken, die auch Dok 30 nicht schliesst.

Schweregrade: 🔴 Blocker (Schema/Code würde nicht laufen oder Finanzzahlen werden falsch) · 🟠 Hoch (fehlende Entität/Kette, blockiert ein Feature) · 🟡 Mittel (Widerspruch/Unklarheit, vor der jeweiligen Phase zu klären) · 🔵 Niedrig (Konsistenz-/Doku-Politur).

Die bereits in den Modulen markierten fachlichen 🔲 zu bestätigen-Punkte (Geschäftsentscheide: MWST-Methode, OCR-Provider, Depot-Frist, Zonen-Taxonomie etc.) sind nicht Gegenstand dieses Dokuments — sie sind in Dok 45 §9 und Dok 40 konsolidiert. Hier geht es um strukturelle Mängel im Datenmodell und in den Modul-Verbindungen.


0. Kurz-Übersicht (priorisiert)

#BefundSchwereBetroffenTyp
F-01RESOLVEDorder_lines/order_items gestrichen; Boxen werden aus quote_lines materialisiert🔴 Blocker4, 5, 3, 30Fehlende Entität
F-02RESOLVEDcreditor_invoice/creditor_payment entfernt; Kreditoren manuell🔴 Blocker6, 30Fehlende Entität / tote Kette
F-03RESOLVEDdeposit_payments.payment_method_id jetzt text🔴 Blocker30DDL-Bug
F-04RESOLVED — Ledger-Trigger setzt invoices.status=PAID/PARTIAL; Affiliate-Trigger hängt daran🔴 Blocker7, 6, 30Tote Auto-Posting-Kette
F-05RESOLVED — überall commission_payout (K-05); affiliate_payout gestrichen🔴 Blocker6, 20, 30Residual-Widerspruch
F-06RESOLVED — Forderung nur bei invoice_issued→INVOICE; quote_accepted→RECEIVABLE entfernt🟠 Hoch4, 6, 30Unklare Auto-Posting-Kette
F-07RESOLVEDrecipients.preferred_lang (Default 'es') ergänzt; SHIPMENT_STATUS zieht Empfänger-Locale daraus🟠 Hoch5, 8, 30Fehlende Verbindung
F-08RESOLVED — Phantom boxes.recipient_contact_id entfernt; KYC-Gate über shipments.recipient_id → recipients🟠 Hoch2, 5, 45Phantom-Spalte
F-09RESOLVED — Rollen-Codes überall UPPERCASE via has_role(); caja_role-ENUM/auth_role() gestrichen🟠 Hoch6, 18, 8, 30Residual-Widerspruch
F-10RESOLVED — einzige Quelle app_users.preferred_locale+theme; user_preferences gestrichen🟠 Hoch18, 8, 30Doppelte Entität
F-11RESOLVEDi18n_labels durch locale_strings + Inline-label_* ersetzt (Affiliate/Design)🟡 Mittel1, 7, 18, 30Residual-Widerspruch
F-12RESOLVED — Konvention: Lookup-*_statuses=UPPERCASE, Inline-CHECK-status (affiliate/commission/closing)=lowercase; in Dok 30 §1 normativ dokumentiert🟡 Mittel4, 6, 7, 30Inkonsistenz
F-13RESOLVED — Lifecycle: Entstehung@invoices→PAID, Freigabe@Prüfung (DELIVERED-Voraussetzung), Auszahlung@Payout; in 16+30 fixiert🟡 Mittel7, 5, 30Unklare Kette
F-14RESOLVEDdeposit_orders.movement_id (Soll) + AP-4 payment_links (Abono→Soll) via deposit_order_id, kein reference-Match🟡 Mittel5, 6, 30Doppelte Buchungslogik
F-15RESOLVEDlead_source als text+CHECK (5 Werte) statt Postgres-ENUM, Validierung erhalten (K-20); 30+1 angeglichen🟡 Mittel1, 30Residual-Widerspruch
F-16RESOLVED — E.164-CHECK für phone_secondary + whatsapp_number ergänzt (30+1)🔵 Niedrig1, 30Validierungslücke
F-17RESOLVED — Mobile-Patterns (Bottom-Sheet/Karten) für Treuhänder-Export, MWST-Settings, Container-Bulk, Affiliate-Mini-Portal in Modul 18 §5.7 ergänzt🟡 Mittel6, 7, 8, 18Fehlendes Mobile-Pattern
F-18RESOLVEDvat_amount_chf wird im Schreibpfad (Server-Action/post_movement) aus vat_rates.rate_pct berechnet + auf Quell-Zeile gespeichert (keine View/generated column); 30+15🟡 Mittel6, 30Unklare Mechanik
F-19RESOLVED — Modul-Refs ergänzt: alle Lookups folgen zentraler RLS-Policy Dok 30 §13 (11+14)🔵 Niedrig2, 5Fehlende RLS (Modul-Ebene)
F-20RESOLVEDnotifications.owner_type-CHECK ergänzt (inkl. recipient/shipment); storage_objects um recipient erweitert (30+17)🔵 Niedrig8, 30Enum-Lücke
F-21🔲 OFFEN → Geschäftsentscheid OE-11 (Rohbild-Aufbewahrung, pending Rechtsgutachten); in 11/18/45 als OE-11 querverwiesen, kein ocr_temp-Bucket🟡 Mittel18, 2, 45Widerspruch Datenhaltung
F-22RESOLVED — Einzige Quelle box_products.is_returnable; boxes.is_returnable abgeleitet (bei Materialisierung, kein Default); 12+14+30🔵 Niedrig3, 5, 30Redundanz
F-23RESOLVED — WhatsApp-Matching auf whatsapp_number mit Fallback phone_primary (K-03); contacts.phone-Wording in Modul 2 korrigiert🔵 Niedrig2, 30Residual-Widerspruch
F-24RESOLVED — Offerten-Empfänger = Freitext (vor KYC, PDF); strukturierter Empfänger erst bei Sendung (shipments.recipient_id); Übergang in 13/14/30 dokumentiert🟡 Mittel4, 5, 30Fehlende Verbindung

1. 🔴 Blocker — Schema/Code läuft nicht oder Finanzzahlen werden falsch

F-01 — order_lines / order_items: referenziert, nirgends definiert

RESOLVED (2026-06-27): Keine eigene Auftragspositions-Tabelle. Boxen werden direkt aus quote_lines der konvertierten Offerte materialisiert (orders.quote_id + Produkt-Snapshot via boxes.box_product_id). order_lines/order_items gestrichen in Modul 4 §4.4/§6.3 und Modul 3 §2/§6.2; Dok 30 §2.6 mit Hinweis ergänzt; invoice_lines als 🔲 Zukunft (MWST je Position).

Fund:

  • Offerten §4.4(b): „Neuen Auftrag anlegen: orders.INSERT … Kopie der Positionen als order_lines".
  • Offerten §6.3: „order_lines werden aus quote_lines kopiert (Produkt, Menge, Preis-Snapshot)".
  • Produkte §2 + §6.2: „order_items → price_list_items.id (eingefrorener Preis bei Auftragsübernahme)" und „order_items.price_list_item_id (FK, eingefroren bei Auftrag aus Offerte)".
  • Dok 30 §1 listet 48 Tabellen — orders ist dabei, order_lines/order_items fehlt vollständig. Es gibt keine DDL für Auftragspositionen in irgendeinem Dokument.

Warum Blocker: Der Kernfluss Offerte→Auftrag→Boxen hängt daran. Ohne Auftragspositionen ist unklar, woraus die Boxen-Materialisierung (Modul 5 §4.1: „pro Offertenposition Anzahl + Box-Typ") ihre Positionen zieht — aus quote_lines der konvertierten Offerte? Oder aus nicht-existenten order_lines? Zusätzlich Namens-Konflikt order_lines (Modul 4) vs. order_items (Modul 3).

Empfehlung:

  1. Entscheiden, ob Auftragspositionen überhaupt eine eigene Tabelle brauchen oder ob boxes (die ohnehin pro Position N-fach entstehen) + der quote_id-Rückbezug genügen. Da Modul 5 die Boxen direkt aus den Offertenpositionen materialisiert und orders.total_chf nur ein read-only-Spiegel ist, ist eine eigene order_lines-Tabelle fachlich evtl. verzichtbar — aber dann müssen Modul 4 §4.4/§6.3 und Modul 3 §6.2 entsprechend korrigiert werden (keine Positions-Kopie, stattdessen Box-Materialisierung).
  2. Falls doch nötig (z. B. für Rechnungspositionen/MWST je Position): kanonische Tabelle order_lines in Dok 30 definieren (analog quote_lines: order_id, position, product_id, product_snapshot, price_list_item_id, unit_price_chf, quantity, Rabattfelder, line_total_chf) und order_items als Jargon streichen.
  3. Konsequenz prüfen für invoices: hat eine Rechnung Positionen? Aktuell ist invoices kopflos (nur total_chf) — bei aktiver MWST je Satz/Position bräuchte es invoice_lines.

F-02 — creditor_invoice / creditor_payment: source_type ohne Quell-Tabelle

RESOLVED (2026-06-27): Kreditoren bleiben manuell. 'creditor_invoice'/'creditor_payment' aus der source_type-Union entfernt (Dok 30 §4.1 + K-05, Modul 6 §3.2); AP-7/AP-8 als „entfällt" markiert. Lieferantenrechnungen/-zahlungen sind manuelle movements (source_type='manual', EXPENSE/SUPPLIER_PAYMENT/SUPPLIER_DEBT). 🔲 strukturierte Kreditoren-Entität als Zukunft notiert.

Fund:

  • movements.source_type-CHECK (Modul 6 §3.2 und Dok 30 §4.1, K-05) enthält 'creditor_invoice' und 'creditor_payment'.
  • Auto-Posting-Matrix Dok 30 §11.1 AP-7: „Lieferantenrechnung erfasst … source_id = creditor_invoice.id"; AP-8: „source_id = payment.id".
  • Es existiert keine Tabelle creditor_invoice, creditor_payment oder „Lieferantenrechnung" in irgendeinem Schema. Kreditoren entstehen heute ausschliesslich als manuelle movements vom Typ EXPENSE/SUPPLIER_DEBT (Modul 6 §3.6, v_kreditoren).

Warum Blocker: source_id von AP-7 zeigt auf eine nicht-existente Zeile → die Idempotenz-Garantie unique(source_type, source_id) ist nicht erfüllbar, das Auto-Posting kann nicht implementiert werden. Die Kette „Lieferantenrechnung → automatische Buchung" ist als Auto-Posting deklariert, aber es gibt nichts, was sie auslöst.

Empfehlung: Eine von zwei Richtungen festlegen und in Dok 30 + Modul 6 konsistent durchziehen:

  • (A) Kreditoren bleiben manuell (realistisch für eine Familien-GmbH ohne Lieferanten-Portal): creditor_invoice/creditor_payment aus der source_type-Union entfernen; AP-7/AP-8 streichen; Lieferantenrechnung/-zahlung sind manuelle movements (source_type='manual'). Das ist der kleinste Eingriff.
  • (B) Echte Kreditoren-Entität einführen (supplier_invoices + supplier_payments analog invoices/Zahlungen) — nur wenn ein strukturierter Kreditoren-Workflow gewünscht ist. Dann Tabellen in Dok 30 definieren.

F-03 — deposit_payments.payment_method_id: uuid referenziert text-PK

RESOLVED (2026-06-27): deposit_payments.payment_method_id von uuid auf text references payment_methods(code) korrigiert (Dok 30 §5) — passt jetzt zum text-PK und zu deposit_refunds. Migration läuft durch.

Fund (Dok 30 §5, Zeile 659):

create table deposit_payments (
  ...
  payment_method_id uuid references payment_methods(code),   -- ❌ uuid → text-PK
  ...
);

payment_methods.code ist text PRIMARY KEY (Dok 30 §3.2). Eine uuid-Spalte kann eine text-PK nicht referenzieren — die Migration schlägt fehl. Direkt darunter macht es deposit_refunds korrekt: payment_method_id text references payment_methods(code) (Zeile 674).

Warum Blocker: DDL-Fehler — 0012_depot.sql (o.ä.) läuft nicht durch.

Empfehlung: Spaltentyp auf text korrigieren und umbenennen in payment_method_code (Konsistenz mit movements.payment_method_code; das Suffix _id ist irreführend für einen Code-FK). Gleiches Muster bei deposit_refunds.payment_method_idpayment_method_code für Einheitlichkeit.


F-04 — invoices.status → PAID hat keinen Auslöser; Affiliate-Provision hängt ins Leere

RESOLVED (2026-06-27): Trigger auf payment_links/movements führt invoices.status nach: Σ zugeordnete Zahlungen ≥ total_chfPAID, >0 <totalPARTIAL, sonst OPEN (Dok 30 §2.7, Modul 6 §4 Workflow C — bewusste Ausnahme von „Status nur aus Views"). Der Affiliate-Provisions-Trigger hängt nicht an PAID, sondern an Sendung DELIVERED (Entscheid #19, 2026-06-28; Modul 16 §4.5); invoices.status dient Mahnwesen/Debitoren.

Fund:

  • Affiliate §4.5 + §6 (Events): „invoice.status → 'paid' └─► IF order.affiliate_id IS NOT NULL └─► INSERT commission_entries". Der gesamte Provisions-Mechanismus startet bei diesem Statuswechsel.
  • Aber: Das Finanzmodul leitet die Restschuld/Bezahlt-Status ausschliesslich aus dem Ledger ab (v_debitoren, v_movements.status_code) und schreibt nie zurück auf invoices.status. Dok 30 §2.7 sagt explizit: „Restschuld wird nicht auf invoices gespeichert, sondern aus dem Ledger abgeleitet". invoices.status (Default OPEN) wird damit von niemandem auf PAID gesetzt.
  • Zusätzlich Schreibweise: Affiliate nutzt 'paid' (lowercase), invoice_statuses definiert PAID (UPPERCASE) — s. F-12.

Warum Blocker: Die Provision wird nie ausgelöst, weil das Trigger-Ereignis nie eintritt. Die zentrale Affiliate-Wertschöpfung (Code → Provision) ist als Kette tot.

Empfehlung:

  1. Festlegen, was den Provisions-Trigger feuert. Optionen (s. auch F-13, Dok 30 §14 offener Punkt): Vollzahlung der INVOICE-Bewegung (= v_debitoren.open_debt_chf = 0 für diese Rechnung), erste Teilzahlung, oder Sendung DELIVERED.
  2. Wenn an „Rechnung bezahlt" festgehalten wird: Es braucht einen expliziten Mechanismus, der invoices.status aus dem Ledger nachführt (Trigger auf payment_links/movements, der bei Voll-Tilgung der zugehörigen INVOICE-Bewegung invoices.status='PAID' setzt) — sonst bleibt invoices.status ein totes Feld. Das ist eine bewusste Design-Entscheidung gegen die „Status nur aus Views"-Regel und muss benannt werden.
  3. Schreibweise auf PAID (UPPERCASE, Lookup-konform) vereinheitlichen.

F-05 — affiliate_payout vs. commission_payout: Widerspruch überlebt die Konsolidierung

RESOLVED (2026-06-27): Kanonisch commission_payout (Dok 30 K-05). affiliate_payout gestrichen in Modul 6 §2/§3.2 und Architektur §8.1 (PostMovementInput) / §8.2 (Event-Tabelle) + §3-Narrativ. Affiliate §6 nutzte bereits commission_payout.

Fund:

  • Dok 30 K-05 (Zeile 19) und §4.1 (Zeile 571) definieren kanonisch 'commission_payout' in der source_type-Union — ohne affiliate_payout.
  • Modul 6 §3.2 (Zeile 178) listet im movements-CHECK aber 'affiliate_payout'. Ebenso Modul 6 §2/§6 und Architektur §8.2 (Zeile 451) + §8.1-Typdef (Zeile 425): durchgehend affiliate_payout.
  • Affiliate §6 (Zeile 464) wiederum nutzt korrekt source_type = 'commission_payout'.

Warum Blocker: Die source_type-CHECK-Constraint kann nur eine Werte-Menge haben. Wird Modul 6 wörtlich genommen, ist commission_payout (das die Affiliate-Auto-Posting-Kette nutzt) kein gültiger Wert → Insert schlägt fehl. Klassischer Fall, wo Modul-Spec und Dok 30 auseinanderlaufen.

Empfehlung: Kanonisch commission_payout (Dok 30 gewinnt). Modul 6 §3.2-CHECK, Modul 6 §2-Tabelle, Architektur §8.1/§8.2 auf commission_payout korrigieren. affiliate_payout ist Jargon und zu streichen. (Triviale, aber zwingende Korrektur — ohne sie läuft das Affiliate-Auto-Posting nicht.)


2. 🟠 Hoch — fehlende Entität/Verbindung, Feature blockiert

F-06 — Doppelzählung Debitoren: quote_accepted→RECEIVABLE + invoice_issued→INVOICE

RESOLVED (2026-06-27): Forderung wird einmalig bei Rechnungsstellung gebucht (invoice_issued → INVOICE). AP-10 (quote_accepted → RECEIVABLE) gestrichen (Dok 30 §11.1); Modul 4 §4.4/§6.4 und Modul 6 §2/§6 entsprechend nachgezogen. Keine Doppelzählung in v_debitoren/v_dashboard mehr.

Fund:

  • AP-10 (Dok 30 §11.1): Offerte angenommen → source_type='quote_accepted'RECEIVABLE (optional) mit total=grand_total_chf, treibt open_debt (Forderung).
  • AP-1: Rechnung gestellt → source_type='invoice_issued'INVOICE mit total=invoices.total_chf (= quotes.grand_total_chf, Dok 30 §2.7), treibt ebenfalls open_debt.
  • Offerten §4.4: Konversion legt in einer Transaktion sowohl den Auftrag (→ quote_accepted) als auch die Rechnung (→ invoice_issued) an. Beide würden buchen.
  • v_debitoren/v_dashboard summieren open_debt_chf über INVOICE und RECEIVABLE.

Warum hoch: Wenn beide AP feuern, erscheint derselbe Forderungsbetrag doppelt in Debitoren und Dashboard — die zentrale „Excel-Parität"-Garantie (Dok 20 §12.2) bricht. Das „(optional)" ist nirgends definiert: Wann genau wird quote_accepted gebucht und wann nicht?

Empfehlung:

  • Klarstellen, dass quote_acceptedRECEIVABLE und invoice_issuedINVOICE einander ausschliessen: Entweder gibt es eine Vormerkung VOR Rechnungsstellung (selten relevant, da Konversion sofort fakturiert) oder direkt die INVOICE. Empfehlung: AP-10 streichen (kein Mehrwert, da bei Konversion sofort INVOICE entsteht) oder explizit auf „nur wenn Auftrag ohne sofortige Rechnung" einschränken — mit Guard gegen gleichzeitige INVOICE derselben reference+contact_id.
  • Falls beide bestehen bleiben: v_debitoren/v_dashboard müssen Doppelzählung verhindern (z. B. RECEIVABLE ignorieren, sobald eine INVOICE mit gleicher reference existiert).

F-07 — Empfänger-DR ohne Sprache, aber SHIPMENT_STATUS-Notification verlangt locale

RESOLVED (2026-06-27): recipients.preferred_lang text not null default 'es' check (in 'de'/'es'/'en') ergänzt (Dok 30 §2.3, Modul 14 §3.3, Modul 1 §3.7). SHIPMENT_STATUS (Modul 17 §6.3) zieht die Empfänger-Locale daraus (Kunde aus contacts.preferred_lang); Default ES deckt den Normalfall ab. Fehlende Telefonnummer → status='skipped' (Plattform §4.6) bleibt abgedeckt.

Fund:

  • Plattform §6.3: Template SHIPMENT_STATUS geht an „Kunde + Empfänger-DR"; notifications.locale ist NOT NULL CHECK in ('de','es','en') und „= Empfänger-Sprache".
  • recipients (Dok 30 §2.3) hat kein preferred_lang/locale-Feld. Empfänger sind keine contacts und keine app_users, also gibt es keine Sprachquelle.
  • Plattform §9 markiert „Woher kommt die Empfänger-locale?" als offen — aber das ist nicht nur eine Opt-in-Frage, sondern eine Schema-Lücke: das Feld existiert nicht.

Warum hoch: Status-Benachrichtigungen an den DR-Empfänger (ein Kern-Use-Case: „tu envío fue entregado") können notifications.locale nicht befüllen. Default es wäre ein stiller Workaround, aber der DR-Empfänger spricht praktisch immer ES — d. h. die Lücke ist tolerierbar, sollte aber bewusst entschieden werden.

Empfehlung:

  1. recipients.preferred_lang text default 'es' check (...) ergänzen (Dok 30 §2.3) oder explizit festlegen: Empfänger-DR-Notifications sind immer ES (dann braucht enqueue_notification für diesen Pfad einen Fix-Wert).
  2. Opt-in/Telefon-Verfügbarkeit klären: recipients.phone_primary ist nullable → bei fehlender Nummer greift status='skipped' (Plattform §4.6) — das ist abgedeckt, aber dokumentieren.

F-08 — boxes.recipient_contact_id: Phantom-Spalte

RESOLVED (2026-06-27): Phantom-Referenz entfernt. Reale Kette boxes.shipment_id → shipments.recipient_id → recipients fixiert; KYC-Gate prüft recipients.cedula_number (bzw. recipients.contact_id → contacts.kyc_status, falls Golden Record). Korrigiert in Modul 2 §6 und Compliance §4.1; Klarstellungs-Note in Dok 30 §7. boxes hat keine recipient_contact_id-Spalte (war reine Doku-Phantomspalte, nie in einer DDL).

Fund:

  • WhatsApp/OCR §6 (Zeile 344): „boxes.recipient_contact_idcontacts.kyc_status muss VERIFIED sein".
  • Compliance §4.1 (Zeile 119): „Empfänger-DR-Daten | contacts (Rolle Empfänger), boxes.recipient_contact_id".
  • Aber: boxes (Modul 5 §3.3 und Dok 30 §7) hat keine Spalte recipient_contact_id. Der Empfänger hängt an shipments.recipient_id → recipients, und recipients.contact_id → contacts (optional). Eine Box kennt ihren Empfänger nur indirekt über shipment_id → shipments.recipient_id.

Warum hoch: Die KYC-Gate-Logik („Box darf nur reisen, wenn Empfänger VERIFIED") referenziert eine nicht-existente Spalte. Die tatsächliche Kette ist boxes → shipments → recipients → contacts.kyc_status (3 Joins, und recipients.contact_id ist nullable → der Empfänger hat evtl. gar keinen contacts-Satz mit kyc_status).

Empfehlung:

  1. Die KYC-Gate-Bedingung auf die reale Kette umformulieren: shipments.recipient_id → recipients.contact_id → contacts.kyc_status. Den Fall recipients.contact_id IS NULL definieren (Empfänger ohne Golden-Record — ist KYC dann am recipients-Satz selbst zu prüfen? recipients.cedula_number existiert).
  2. Modul 2 §6 und Compliance §4.1 entsprechend korrigieren (kein boxes.recipient_contact_id).
  3. Grundsatzfrage: Soll KYC am Empfänger (recipients) oder am verknüpften Kontakt hängen? recipients hat eigene cedula_number/cedula_scan_path, aber kein kyc_status-Feld → evtl. fehlt recipients.kyc_status.

F-09 — Rollen-Codes: drei divergente Schreibweisen

RESOLVED (2026-06-27): UPPERCASE-Codes ADMIN, BUCHHALTUNG, OPERATIONS, FAHRER, AFFILIATE, READONLY überall. Modul 18 §3.3 caja_role-ENUM gestrichen (→ roles-TEXT-Lookup); Modul 6 §3.7 auth_role()/Personennamen → has_role('CODE'); Modul 16 RLS-Bullets auf UPPERCASE. Mapping admin_office→BUCHHALTUNG, operations→OPERATIONS, driver→FAHRER dokumentiert.

Fund:

  • Modul 8 / Dok 30 (kanonisch, K-17): ADMIN, BUCHHALTUNG, OPERATIONS, FAHRER, AFFILIATE, READONLY (UPPERCASE), RLS via has_role('CODE').
  • Modul 18 §3.3: CREATE TYPE caja_role AS ENUM ('admin','operations','admin_office','driver','affiliate','readonly')lowercase, andere Namen (admin_office statt BUCHHALTUNG, operations/driver statt OPERATIONS/FAHRER), und als Postgres-ENUM (widerspricht K-20: erweiterbare Mengen = TEXT-Lookup).
  • Modul 6 §3.7: RLS über Personennamen marcel/mariela/markus/arkys + Helper auth_role() (statt has_role()).
  • Architektur §6.3 benennt diesen Konflikt selbst als „⚠️ Schema-Inkonsistenz (zu reconcilen)" und erklärt Modul 8 zur Wahrheit — aber Modul 6 und 18 sind im Text nicht nachgezogen.

Warum hoch: Wer Modul 6 oder 18 wörtlich implementiert, baut ein zweites, inkompatibles Rollenmodell. RLS-Policies aus Modul 6 (auth_role() in ('marcel',...)) funktionieren mit dem kanonischen roles/user_roles-Schema nicht.

Empfehlung: Modul 8/30 ist normativ (steht so in Dok 20 §6.3). Konkret:

  1. Modul 18 §3.3: caja_role-ENUM streichen (Rollen sind roles-Lookup mit TEXT-Code).
  2. Modul 6 §3.7: alle auth_role()-Policies auf has_role('ADMIN'|'BUCHHALTUNG'|...) umschreiben; marcel/mariela/markus/arkys sind nur Referenz.
  3. admin_officeBUCHHALTUNG mappen (Mariela). Einmaliges Mapping-Statement dokumentieren.

F-10 — user_preferences (Modul 18) vs. app_users (Modul 8/30)

RESOLVED (2026-06-27): Einzige Quelle = app_users. theme text default 'system' zu app_users ergänzt (Dok 30 §6.1); user_preferences (Modul 18 §3.2) gestrichen; Workflows §4.1/§4.2 + Sprachdeklaration auf app_users.preferred_locale/theme umgeschrieben. Default-Sprache überall 'es' (sidebar_collapsed = reine Client-Pref/localStorage).

Fund:

  • Modul 18 §3.2 definiert user_preferences (user_id, lang, theme, sidebar_collapsed) mit Default lang='de'.
  • Modul 8/30 hält app_users.preferred_locale (Default 'es') als Sprachquelle; Theme/Sidebar sind in Modul 8 nicht modelliert.
  • Konflikt zusätzlich im Default: Modul 18 lang='de', Modul 8/30 'es' (und der Brief sagt ES-priorisiert). Architektur §6.3 empfiehlt: Theme/Sidebar in app_users integrieren, user_preferences fällt weg — aber Modul 18 ist nicht nachgezogen.

Warum hoch: Zwei konkurrierende Sprachquellen → undefiniert, welche bei Konflikt gewinnt; widersprüchlicher Default (DE vs. ES). theme/sidebar_collapsed existieren nur in Modul 18.

Empfehlung: app_users um theme text default 'system' (+ optional sidebar_collapsed) erweitern und in Dok 30 §6.1 aufnehmen; user_preferences (Modul 18) streichen. Default-Sprache überall 'es'. Modul 18 §4.1/§3.2 darauf umschreiben.


3. 🟡 Mittel — Widersprüche/Unklarheiten, vor Phase zu klären

F-11 — i18n_labels vs. locale_strings: Seeds zeigen in tote Tabelle

RESOLVED (2026-06-27): i18n_labels wird nicht gebaut (K-16). Modul 18 §3.1 CREATE TABLE i18n_labelslocale_strings-Doku + Hybrid; Modul 16 §3.7 INSERT INTO i18n_labelsINSERT INTO locale_strings (namespace,key,locale,value); Modul 13 Status-Lookup-Hinweis auf Inline-label_*/locale_strings. (Modul 1 nutzt contact_role_labels, kein i18n_labels — keine Änderung nötig.)

Fund: Dok 30 K-16 entscheidet eindeutig: „i18n_labels wird NICHT gebaut" — Inline-label_de/es/en an Lookups + locale_strings für freie Texte. Aber Modul 1 §3.10 (contact_role_labels), Modul 7 §3.7 (INSERT INTO i18n_labels …) und Modul 18 §3.1 (CREATE TABLE i18n_labels) bauen/befüllen weiterhin i18n_labels. Warum mittel: Wer diese Module wörtlich nimmt, seedet in eine nicht-existente Tabelle. Empfehlung: Modul 1/7/18 auf den Hybrid (Inline-Labels + locale_strings) umschreiben; i18n_labels-Inserts → locale_strings (namespace = entity), wie Dok 30 K-16 vorgibt.

F-12 — Status-Schreibweisen Gross/Klein gemischt

RESOLVED (2026-06-27): Konvention projektweit fixiert (Dok 30 §1, normativ): Lookup-Tabellen-Codes (*_statuses, tracking_statuses, movement_types, payment_methods, roles) sind UPPERCASE; Inline-CHECK-status-Werte ohne eigenen Lookup (affiliates, commission_entries, commission_payouts, monthly_closings, deposit_*-kind) sind lowercase. Bewusste Regel statt Zufall. Quote/Invoice-Codes sind UPPERCASE (Lookup).

Fund: quote_statuses = DRAFT/SENT/ACCEPTED/... (UPPERCASE), invoice_statuses = OPEN/PARTIAL/PAID/... (UPPERCASE, Dok 30), aber affiliates.status/commission_entries.status/commission_payouts.status = active/paused/draft/approved/paid/... (lowercase, Modul 7 + Dok 30 §8). Warum mittel: Inkonsistente Konvention erschwert generische Status-Badge-/i18n-Logik und lädt zu if status === 'paid'-vs-'PAID'-Bugs ein (s. F-04). Empfehlung: Eine Konvention projektweit. Da die meisten Lookups UPPERCASE-Codes nutzen (Glossar-Prinzip), Affiliate-Status auf UPPERCASE ziehen — oder bewusst dokumentieren, dass Enum-text-Werte lowercase und Lookup-Codes UPPERCASE sind (klare Regel statt Zufall).

F-13 — commission_entries-Trigger-Zeitpunkt unbestimmt

RESOLVED (2026-06-27): Lifecycle eindeutig (Dok 30 §8, Modul 16 §4.5/§4.6, Modul 14 §6): (a) Entstehung INSERT pending bei invoices.status → PAID (Ledger-Trigger, konsistent mit F-04 — einziger Auslöser); (b) Freigabe → approved bei der Abrechnungsprüfung (Sendung DELIVERED ist fachliche Voraussetzung, kein DB-Trigger); (c) Auszahlung → paid beim Payout (AP-9). Frühere „fällig bei DELIVERED"-Formulierung (Modul 5) = Freigabe-Voraussetzung, nicht Entstehung. 🔲 Voll-/Teilzahlung bleibt Geschäftsentscheid (Default Vollzahlung).

Fund: Affiliate §4.5 sagt „bei invoice.status='paid' (oder erste Zahlung, 🔲)", Modul 5 §6 + Dok 30 §11 sagen „Provision fällig wenn Sendung DELIVERED", Roadmap P3 sagt „commission_entries.status → 'approved' wenn DELIVERED". Drei verschiedene Zeitpunkte für Entstehung (INSERT pending) vs. Freigabe (approved) vs. Fälligkeit. Warum mittel: unklar, ob bei Zahlung oder Zustellung gebucht wird; hängt mit F-04 zusammen. Empfehlung: Lifecycle explizit: (a) INSERT pending bei Ereignis X, (b) → approved bei Ereignis Y, (c) → paid bei Payout. Die drei Ereignisse einmal festnageln (Dok 30 §14 listet es bereits als offen — hier die konkrete Stelle).

F-14 — Depot: Soll- und Abono-Buchung, Restschuld-Match unklar

RESOLVED (2026-06-27): Explizite Verknüpfung statt reference-Match. deposit_orders.movement_id (Soll, AP-3) ergänzt (Dok 30 §5). AP-4 erzeugt payment_links(payment_id=Abono, target_id=deposit_orders.movement_id, amount); der Soll wird über deposit_payments.deposit_order_id → deposit_orders.movement_id eindeutig gefunden (analog AP-2/INVOICE). So finden Soll+Abono in v_anzahlungen sicher zusammen. Fixiert in Dok 30 §11.1, Modul 14 §4.5, Modul 15 §4 Workflow B.

Fund: AP-3 bucht deposit_orderDEPOSIT_CASH (Soll, total=deposit_total_chf, paid=0); AP-4 bucht deposit_paymentDEPOSIT_CASH (Abono, paid=amount_chf). Beide sind DEPOSIT_CASH. Die Restschuld-/Status-Logik in v_movements matcht offene Posten über reference + contact_id oder payment_links. Warum mittel: Für die Depot-Soll-Bewegung (AP-3) und ihre Abono (AP-4) ist nicht definiert, ob sie dieselbe reference tragen oder via payment_links verknüpft werden. Ohne das rechnet v_anzahlungen.pending_chf falsch (Soll und Abono finden sich nicht). Modul 6 §B beschreibt nur die Abono, nicht die Soll-Verknüpfung. Empfehlung: Für Depot denselben payment_links-Mechanismus wie für INVOICE/INVOICE_PAYMENT vorschreiben (AP-4 erzeugt payment_links(payment_id, target_id=DEPOSIT_CASH-Soll-movement)), analog zu AP-2. Dok 30 §11.1 „Begleit-Updates" erwähnt das für AP-2, nicht für AP-4 — ergänzen.

F-15 — contacts.lead_source: ENUM-Validierung verloren

RESOLVED (2026-06-27): lead_source ist eine text-Spalte mit CHECK in ('website_form','whatsapp','manual','import','referral') (K-20: erweiterbare Menge als TEXT, kein Postgres-enum) — Validierung bleibt erhalten. Angeglichen in Dok 30 §2.1 und Modul 1 §3.2/§3.5 (CREATE TYPE → Werte-Referenz + TEXT+CHECK).

Fund: Modul 1 §3.2 definiert lead_source_type als Postgres-ENUM (website_form/whatsapp/manual/import/referral). Dok 30 §2.1 macht daraus lead_source text (freier Text, nur Kommentar nennt die Werte) — vermutlich wegen K-20 (Enums → Lookups), aber hier wurde weder ein Lookup angelegt noch ein CHECK gesetzt. Warum mittel: Tippfehler/ungültige Quellen landen ungeprüft in der Spalte. Empfehlung: Entweder check (lead_source in ('website_form','whatsapp','manual','import','referral')) ergänzen oder konsistent als Lookup lead_sources führen (wie die anderen *_statuses).

F-17 — Fehlende Mobile-Patterns für mehrere Screens

RESOLVED (2026-06-27): Modul 18 §5.7 ergänzt mit <sm-Patterns für Treuhänder-Export (Bottom-Sheet + signierte-URL-Download via Share-Sheet, serverseitige Generierung gegen Perf-Risiko), MWST-Aktivierung/Settings (Karten-Liste + Confirm-Sheet), Container-Detail-Bulk (Long-Press-Select, sticky Bulk-Bottom-Bar, Inline-Kapazitäts-Alert) und Affiliate-Mini-Portal (reduzierte 3-Tab-Shell, nur eigene Daten). Preis-Resolver-Testscreen bleibt bewusst Desktop-only (dokumentiert).

Fund: Mobile/Tablet-First ist „harte Kern-Pflicht", aber für folgende Screens fehlt das adaptive Handy-Pattern (kein Karten-/Bottom-Sheet-Layout spezifiziert, nur Desktop-Dialog):

  • Treuhänder-Export (Modul 6 §5.7): nur als „Dialog" beschrieben — am Handy Bottom-Sheet? Datei-Download auf Mobile-PWA?
  • MWST-Aktivierung / Settings (Modul 6 §F): kein Screen-Pattern.
  • Container-Detail Bulk-Aktionen (Modul 5 §5.5): „Multi-Select (Long-Press am Handy)" erwähnt, aber Bulk-Bar-Verhalten/Verladen-Flow am 375px-Handy nicht ausgeführt; Kapazitäts-Warnung als Inline-Alert auf Karten?
  • Affiliate-Mini-Portal (Modul 7 §9, (portal)): komplett offen — eigene reduzierte Mobile-Shell undefiniert.
  • Preis-Resolver-Testscreen (Modul 3 §5.5): „nur Desktop/Tablet" — bewusst, aber als Lücke dokumentieren.

Warum mittel: Verstösst potenziell gegen die harte Mobile-Pflicht. Empfehlung: Für jeden Screen das <sm-Pattern ergänzen (Bottom-Sheet/Karten gemäss Modul 18 §5). Export-Download auf PWA explizit testen (F-17 ist auch ein Perf-/Funktions-Risiko: grosse CSV/PDF auf Mittelklasse-Android).

F-18 — vat_amount_chf „beim Speichern herausgerechnet" — wo?

RESOLVED (2026-06-27): Verortet in der schreibenden Schicht: Server-Action bzw. post_movement() (für quellen-erzeugte Buchungen) liest vat_rates.rate_pct zum vat_code und schreibt vat_amount_chf als persistierte Spalte auf die Quell-Zeile (movements/invoices/quotes). Keine View und keine generated column (da rate_pct in Fremdtabelle). Idempotent beim Upsert. Fixiert in Dok 30 §4.1 und Modul 15 §3.2/§4 F.

Fund: Modul 6 §F + Compliance §3.4: „vat_amount_chf wird beim Speichern berechnet, wenn vat_code gesetzt". Aber movements ist nach Doktrin ein Eingabewert-Ledger; alle Ableitungen leben in Views (Dok 30 §4.4). vat_amount_chf ist eine gespeicherte Spalte, kein View-Wert. Warum mittel: Unklar, ob die MWST-Berechnung in einem Trigger, in der Server-Action oder in der Auto-Posting-Engine läuft — und ob sie für quellen-erzeugte (Upsert-)Buchungen idempotent ist. Empfehlung: Verorten: MWST-Betrag in der schreibenden Schicht (Server-Action/post_movement) berechnen, nicht als View (da pro Zeile aus vat_rates.rate_pct ableitbar, wäre auch eine generated column denkbar, aber rate_pct liegt in anderer Tabelle → Trigger oder Schreibpfad). Einmal festlegen.

F-21 — OCR-Bild: „sofort löschen" vs. „aufbewahren"

🔲 OFFEN — Geschäftsentscheid OE-11 (pending Rechtsgutachten), NICHT technisch auflösbar. Die Aufbewahrungsdauer des KYC-Rohbilds ist eine Rechts-/Geschäftsfrage (Zoll/Geldwäsche DR+CH vs. revDSG-Minimierung) → mit Treuhänder/Rechtsberatung zu klären (OE-11). Technische Vereinheitlichung erfolgt (2026-06-27): kein ocr_temp-Bucket (Dok 30 §6.3 kennt nur kyc/documents/receipts/public-assets); Rohbild liegt im privaten kyc-Bucket; Default bis OE-11 = minimal (Löschung nach KYC-Zweckerfüllung). In Modul 18 §5.5/§8.1, Modul 11 §8.1 (Open Point 6) und Compliance §4.6/§9.2 konsistent als OE-11 querverwiesen. Bleibt als Business-Decision offen.

Fund: Modul 18 §5.5 + §8.1: „Original-Bild wird nach OCR serverseitig gelöscht (nur extrahierte Felder gespeichert)" und „ocr_temp-Bucket Auto-Delete nach 24h". Modul 2 §8 + Compliance §4.6: Rohbild im kyc-Bucket „so kurz wie möglich nach KYC-Zweckerfüllung" (also nicht zwingend sofort) und potenziell fristrelevant fürs Zolldossier. Dok 30 nennt nur kyc-Bucket, kein ocr_temp. Warum mittel: Widersprüchliche Aufbewahrung (sofort/24h vs. bis Zweckerfüllung) und ein in Dok 30 nicht existenter Bucket (ocr_temp). Empfehlung: Eine Linie: Rohbild in kyc-Bucket, Aufbewahrung = Compliance §4.6 (retain_until minimal, Lifecycle-Job). ocr_temp-Bucket + „sofort löschen" aus Modul 18 streichen oder als reine Transient-Stufe vor kyc-Persistenz definieren. Konsistent mit Storage-Bucket-Liste (Dok 30 §6.3: nur kyc/documents/receipts/public-assets).

F-24 — quotes.recipient_name/-address (Freitext) vs. recipients-Entität

RESOLVED (2026-06-27): Dokumentiert: Offerten-Empfänger ist Freitext (recipient_name/recipient_address, früh/vor KYC, rein fürs PDF, kein FK). Der strukturierte DR-Empfänger entsteht erst bei der Sendung (shipments.recipient_id → recipients, Modul 14 §4.3); der Freitext kann dort als Vorschlag vorbelegt werden, wird aber nicht automatisch zur recipients-Zeile. Übergang fixiert in Dok 30 §2.5, Modul 13 §3.x, Modul 14. 🔲 Optionale quotes.recipient_id → recipients bleibt spätere Ausbaustufe.

Fund: quotes trägt recipient_name/recipient_address als Freitext „Anzeigename PDF (kann abweichen)". recipients ist die strukturierte DR-Empfänger-Entität, die erst bei der Sendung (shipments.recipient_id) gesetzt wird. Es gibt keine Verbindung zwischen dem Offerten-Freitext-Empfänger und dem späteren strukturierten recipients-Satz. Warum mittel: Beim Übergang Offerte→Auftrag→Sendung muss der Empfänger neu/strukturiert erfasst werden; der auf der Offerte genannte Empfänger geht verloren bzw. wird nicht übernommen. Empfehlung: Entweder dokumentieren, dass der Offerten-Empfänger rein kosmetisch (PDF) ist und die Sendung den Empfänger unabhängig erfasst, oder eine optionale quotes.recipient_id → recipients vorsehen, die bei der Sendungsbildung vorgeschlagen wird.


4. 🔵 Niedrig — Konsistenz-/Doku-Politur

F-16 — phone_secondary ohne Format-Constraint

RESOLVED (2026-06-27): E.164-CHECK ergänzt für contacts.phone_secondary und contacts.whatsapp_number (chk_phone_secondary_e164/chk_whatsapp_number_e164) in Dok 30 §2.1 + Modul 1 §3.5. recipients.phone_* bewusst ohne harten CHECK belassen (DR-Nummern werden teils uneinheitlich erfasst; Normalisierung im Schreibpfad) — als bewusste Entscheidung dokumentiert.

contacts.phone_primary hat chk_phone_primary_e164; phone_secondary, whatsapp_number und recipients.phone_* hatten keinen E.164-CHECK.

F-19 — Lookup-RLS nur in Dok 30, nicht in Einzelmodulen

RESOLVED (2026-06-27): Einzeilige Verweise ergänzt — Modul 2 (§3.2 kyc_document_types) und Modul 5 (§3.6, alle Logistik-Lookups inkl. tracking_phases/deposit_refund_kinds) verweisen jetzt explizit auf die zentrale RLS-Policy in Dok 30 §13 (CRU für ADMIN, R für Rest), damit „enable RLS + Policy" pro Tabelle in derselben Migration nicht vergessen wird (Dok 20 §9.2 Review-Gate).

F-20 — Polymorphe owner_type-Listen unvollständig

RESOLVED (2026-06-27): notifications.owner_type-CHECK ergänzt mit allen real referenzierten Owner-Typen ('contact','recipient','quote','order','invoice','shipment','movement','commission_payout') (Dok 30 §6.3 + Modul 17 §3.11). storage_objects.owner_type (Modul 17 §3.10) um recipient erweitert (war in Dok 30 bereits enthalten). Damit kann SHIPMENT_STATUS an Empfänger (owner_type='recipient'/'shipment') sauber referenzieren.

F-22 — is_returnable doppelt (boxes und box_products)

RESOLVED (2026-06-27): Einzige Quelle = box_products.is_returnable (Modul 12 §3.2 ergänzt, Dok 30 §3.3). boxes.is_returnable ist abgeleitet: wird bei der Box-Materialisierung (Modul 14 §4.1) aus box_products.is_returnable initialisiert; der widersprüchliche boxes-Default true wurde entfernt (not null, kein Default) in Dok 30 §7 und Modul 14 §3.3.

F-23 — WhatsApp-Matching-Feld

RESOLVED (2026-06-27): Auf K-03 ausgerichtet. Modul 2 §4.2 (Workflow B Schritt 4) matcht jetzt gegen contacts.whatsapp_number mit Fallback contacts.phone_primary (contacts.phone existiert kanonisch nicht). Konsistent mit Dok 30 K-03 und der chk_whatsapp_number_e164-Validierung (F-16).


5. Was gut/vollständig ist (zur Einordnung)

Damit die Liste nicht den falschen Eindruck erweckt — das Pflichtenheft ist überdurchschnittlich konsistent:

  • Dok 30 (Kanonisches Schema) löst die grosse Mehrheit der naiven Modul-Konflikte sauber über K-01..K-21 auf (Namensvereinheitlichung price_list_items, orders.customer_id, commission_entries, zones, app_users vs. contacts-Trennung etc.). Die meisten Befunde oben sind Residual-Fälle, wo nur der Modul-Text nicht nachgezogen wurde.
  • Auto-Posting-Idempotenz (unique(source_type, source_id) + Upsert) ist durchgängig und korrekt verankert — die Lücken betreffen einzelne Quell-Definitionen (F-02, F-04), nicht das Prinzip.
  • Revisionssicherheit (append-only audit_log via Rules, Periodensperre, kein hartes DELETE) ist konsistent über Module 6/8 + Compliance.
  • RLS-Gesamtmatrix (Dok 30 §13) ist vollständig und deckt alle 48 Tabellen × 6 Rollen ab — die Affiliate-Mandantentrennung ist mehrfach abgesichert.
  • Migrations-Topologie (Dok 30 §9) berücksichtigt die Zirkulär-FKs (contacts↔kyc_scans, quotes↔orders, invoices↔movements) korrekt.
  • Compliance-Matrix (Dok 45) verankert jede OR-957/GeBüV/revDSG/MWST-Anforderung an konkreten Entitäten.

6. Empfohlene Sofort-Massnahmen (vor Implementierungsstart)

Reihenfolge nach Risiko/Aufwand:

  1. F-05, F-11, F-09 (erledigt 2026-06-27): Modul-Specs 6/16/18 auf Dok 30 nachgezogen (commission_payout; locale_strings statt i18n_labels; UPPERCASE-Rollen via has_role() statt caja_role/auth_role()).
  2. F-03 (erledigt 2026-06-27): deposit_payments.payment_method_id auf text references payment_methods(code) korrigiert (Dok 30 §5).
  3. F-01, F-02, F-04 (erledigt 2026-06-27): keine order_lines (Box-Materialisierung aus quote_lines); Kreditoren manuell; Invoice-PAID via Ledger-Trigger.
  4. F-06, F-14 (erledigt 2026-06-27, Finanz-Korrektheit): Doppelzählung RECEIVABLE/INVOICE durch Streichung von AP-10 ausgeschlossen; Depot-Soll/Abono-Verknüpfung via deposit_orders.movement_id + payment_links (F-14) fixiert.
  5. F-07, F-08, F-24 (erledigt 2026-06-27, Empfänger-Kette): recipients.preferred_lang; KYC-Gate auf reale Join-Kette shipments.recipient_id → recipients; Offerten-Freitext vs. strukturierter Sendungs-Empfänger dokumentiert.
  6. F-17 (erledigt 2026-06-27, Mobile-Pflicht): Handy-Patterns für Treuhänder-Export, MWST-Settings, Container-Bulk, Affiliate-Mini-Portal in Modul 18 §5.7 ergänzt.
  7. F-12, F-13, F-15, F-16, F-18, F-19, F-20, F-22, F-23 (erledigt 2026-06-27): Status-Konvention, Provisions-Lifecycle, lead_source-CHECK, E.164-CHECKs, MWST-Schreibpfad, Lookup-RLS-Refs, owner_type-CHECKs, is_returnable-Quelle, WhatsApp-Matching.
  8. 🔲 F-21 → Geschäftsentscheid OE-11 (Rohbild-Aufbewahrung, pending Rechtsgutachten): technisch vereinheitlicht (kein ocr_temp, kyc-Bucket, Default minimal); rechtliche Frist bleibt offen.

7. IST-Gap-Folds & Entscheide (Alt-ERP-Analyse, 2026-06-27)

Aus der verifizierten Gap-Analyse des Alt-ERP „LCCargo" (docs/legacy-ist/95-caja-gap-analyse.md; adversarial geprüft: 8 Lücken bestätigt, 2 teilweise, 0 widerlegt). Hier die Spec-relevanten Konsequenzen.

7.1 Getroffene Geschäftsentscheide

#EntscheidWahlUmgesetzt in
E7TarifmodellHybrid — Festpreise je Box-Typ × Zone + Volumen-Tarif für Übergröße/OTRO (B/H/L pro Box).Dok 30 §3.3 (OTRO, boxes.*_cm), Modul 3 §4 (Resolver)
E4Personen-Stand auf BelegEinfrieren (Snapshot) — zoll-/revisionssicher, überlebt Kontakt-Merge.Dok 30 §2.7/§7 (bill_to_snapshot, sender_snapshot, recipient_snapshot)
E8Endkunden-PortalPhase 2+ — kein Kundenlogin zum Start; RLS-Helfer current_contact_id() von Anfang an mitdenken.Modul 17 §10.5

7.2 Verifizierte P0-Lücken — gefoldet

GapIST-RegelAnforderungStatus
G1BR-30Personen-Snapshot (Absender/Empfänger) auf Sendung+Rechnung einfrieren✅ gefoldet (Dok 30 §2.7/§7)
G2BR-39/40/41Zoll-/Aduana-Belege + Sendungsliste + Manifest-View✅ gefoldet (Modul 14 §10.1, doc_class erweitert)
G3BR-20Zweiter Wiegepunkt weight_rd_kg (RD)✅ gefoldet (Dok 30 §7, Modul 14 §10.2)
G4BR-48Zollfelder (Documento Embarque = containers.bl_number; Dokumenttyp Absender)🟡 teilw. (BL vorhanden; Absender-Dokumenttyp im Manifest §10.1)
G5BR-33/053-Fahrer-Zuordnung + Fahrer-RLS BR-05✅ gefoldet (Dok 30 §7, Modul 14 §10.4) — Entscheid E10 offen
G6BR-28Unterschrift-Liefernachweis (signer_name + doc_class='signature')✅ gefoldet (Dok 30 §7, Modul 14 §10.3) — Pflicht/optional E11 offen
G7BR-23Volumen-Tarif für Freimaß neben Box-Festpreisen✅ gefoldet (E7-Hybrid, Dok 30 §3.3)
G8BR-S12CSRF-/Origin-Schutz explizit✅ gefoldet (Modul 17 §10.1)
G9BR-S07Application-Error-Logging (separat vom audit_log)✅ gefoldet (Modul 17 §10.3)
G10BR-08/12Status-Mapping 10→9 + Datums-Semantik je Status (View)✅ gefoldet (Modul 14 §10.5) — DR_DEPOT Entscheid E3 ✅ (2026-06-28)
G-SetBR-S08app_settings-Tabelle für globale Flags✅ gefoldet (Dok 30 §6.1, Modul 17 §10.2)

7.3 Completeness-Korrekturen (aus der Verifikation)

  • BR-06 (Invoice-spezifisches Admin-Always-Allow) und BR-P12 (Fahrer-Flag-Nullability) waren in der Gap-Matrix nicht gemappt — als reale Zusatz-Befunde vermerkt; durch Cajas deklaratives has_role('ADMIN') (BR-06) bzw. echtes Rollenmodell (BR-P12) abgedeckt.
  • Klassifikations-Schärfung: BR-S07 (Error-Log) → 🔴 (G9), BR-S08 (Settings) → 🔴/🟡 (G-Set), BR-12 (Datums-Semantik) → 🔴 (G10).

7.4 Verbleibende offene Geschäftsentscheide

Alle Entscheide getroffen (2026-06-28). Vollständige Übersicht:

E3 DR_DEPOT als Tracking-Schritt 9 (§7.6 Entscheid 9) · ✅ E5 Nachrichten an CH-Absender und DR-Empfänger; kein Zahlungs-Gate (§7.7 Entscheid 16/17) · ✅ E6 Box-Sortiment korrekt, Bild je Typ aktiv (§7.6 Entscheid 13 + §7.7 E6) · ✅ E9 Soft-Unique E-Mail/Telefon: Warnung + Merge-Vorschlag, kein stilles Duplikat (§7.7 Entscheid 20) · ✅ E10 3 Fahrer-Rollen beibehalten (§7.6 Entscheid 11) · ✅ E10b Fahrer-FK → app_users (alle Fahrer haben Caja-Login; §7.6 Entscheid 11) · ✅ E11 Liefernachweis: Empfänger wählt Methode (Unterschrift / Abstellerlaubnis+Foto / Selbstdeklaration; §7.6 Entscheid 12) · ✅ E12 Keine RD-Nachwiegung (weight_rd_kg optional/deprecated; §7.6 Entscheid 10) · ✅ E13 MwSt aktiv, inklusiv, Versand EXEMPT; Mehrwährung CHF+DOP via Revolut (§7.6 Entscheid 1–3, §7.7 Entscheid 3) · ✅ E14 app_users↔contacts-Brücke: getrenntapp_users = Login-Identität (1:1 zu auth.users), contacts = Geschäfts-Partei; Brücke via affiliate_users(user_id→app_users, affiliate_id→affiliates) (Dok 30 K-18) · ✅ E15 GS1-Mitgliedschaft beschafft → SSCC aktiv (§7.7 Entscheid 22)

Beyond-IST (Erweiterung): Caja ist GS1-SSCC-ready (Modul 14 §12, boxes.sscc, GS1-Settings). E15 entschieden (2026-06-28): GS1-Schweiz-Mitgliedschaft wird beschafft → SSCC aktiv (gs1_sscc_enabled=true sobald GCP vorliegt); bis dahin läuft alles über UUID/QR — kein Schema-Umbau nötig.

7.5 Feature-Parität Welle 2 (Action-/Listen-/Report-/Screen-Ebene)

Nach den P0-Folds (§7.2) wurden die verbleibenden Detail-Features des Alt-ERP gefoldet — damit ist die Spec feature-vollständig gegenüber dem Alt-System (jede Controller-Action, jeder Screen, jede Geschäftsregel vertreten):

BereichIST-FeatureVertreten in
LogistikNotification-Trigger-Matrix (BR-15, inkl. Zahlungs-Gate)Modul 14 §11.1
LogistikBulk-Statuswechsel mit History+Mail (BR-16)Modul 14 §11.2
LogistikBox-Stammdaten-Sperre = IsSent (BR-22/26)Modul 14 §11.3
LogistikFahrer-Picker + Fahrer-RLS (BR-33/05)Modul 14 §11.4, Dok 30 §13 Fn 12
LogistikStatus-Zähl-Badges (BR-38/43)Modul 14 §11.6 (v_tracking_status_counts)
LogistikDatumsfilter inkl. + Sortierung (BR-35/37/44)Modul 14 §11.7
LogistikReport-Export-Modi + Belegkopf + Absender-Doktyp (BR-40/41/48)Modul 14 §11.8, Dok 30 §2.1 id_doc_type
OffertenPreis-Override-Recht serverseitig (BR-25)Modul 13 (Nachtrag)
CRMalias-Feld (BR-P16)Dok 30 §2.1/§2.3
CRMEmpfänger→Kunde-Promotion (BR-P15)Modul 10 (Nachtrag), recipients.promoted_contact_id
CRME-Mail-Dedup-Doku, Telefon-Mapping (BR-P03/P16)Modul 10 (Nachtrag), Dok 30 §2.1
PlattformSettings-Screen (IST Settings.html)Modul 17 §11.1
PlattformRollen-/Rechte-Matrix read-only (IST Roles.html)Modul 17 §11.2
PlattformObjekt-Verlauf-Tab (IST „Historial de cambios")Modul 17 §11.3, Modul 10
PlattformLogout / PW-Reset-Divergenz / eigene E-Mail (BR-S06/P04)Modul 17 §11.4

Verbleibend sind ausschließlich die Geschäftsentscheide (§7.4) — keine offenen Feature-Lücken mehr.

7.6 Geschäftsentscheide getroffen (2026-06-28, Marcel)

#EntscheidUmsetzung
1MwSt ab sofort aktivapp_settings.vat_enabled=true; Modul 6 MwSt scharf
2Preise inkl. MwSt (brutto)vat_method='inclusive'; MwSt wird aus Bruttopreis herausgerechnet
3Mehrwährung CHF + DOP; DOP-Zahlung via Revolut Businesscurrency/fx_rate im Ledger; neue Integration Revolut Business (Modul 6 + Architektur)
4Anfangssaldo Kasse/Bank🔲 Betrag folgt von Marcel
5KYC-Aufbewahrung: max. 24 Monate nach letzter Interaktionretain_until = last_interaction + 24M; Auto-Löschung
6Drittland-/OCR-Übertragung erlaubt (Einwilligung im Onboarding-Vertrag)Onboarding-Flow erfasst/referenziert Consent (Modul 2 + 45)
7Datenschutzerklärung (CH/revDSG): jaEntwurf 46-datenschutz.md (juristisch zu prüfen)
8Aufbewahrung CH-Recht (OR 957/GeBüV): ja10 J. Belege; in Modul 45 verbindlich
9DR-Lager: janeuer Tracking-Status DR_DEPOT (zw. DR_CUSTOMS und DELIVERED)
10Keine RD-Nachwiegungboxes.weight_rd_kg nicht aktiv genutzt (Feld bleibt optional/deprecated)
113 Fahrer-Rollen: ja; alle Fahrer bedienen die AppFK auf app_users (kein Fahrer ohne Login) — E10/E10b gelöst
12Liefernachweis: Empfänger wähltMethode je Zustellung: Unterschrift · Ablage/Deponierung mit Foto (CH-Post-Style Abstellerlaubnis) · Selbstdeklaration
13Bild pro Box-Typ: ja (Bilder extern)box_products.image_path; Bilder TBD (nicht von Marcel)
14DR-Preiszonen🔲 folgt von Marcel
15Rundung: immer aufrunden auf ganze ZahlenPreis-Resolver ceil auf ganze CHF

7.7 Geschäftsentscheide getroffen (2026-06-28 Teil 2, Marcel)

#EntscheidUmsetzung
16Status-Nachrichten an beide (CH-Absender + DR-Empfänger)Notification-Matrix (Modul 14 §11.1, Modul 17 §6)
17Keine Unterdrückung bei offener RechnungRevision: Zahlungs-Gate entfernt — Status-Nachrichten gehen immer
18Provision = % je Affiliate/Sublieferantreferral_codes/commission_entries commission_kind='percent', Satz je Affiliate konfigurierbar
19Provision fällig bei Zustellung (DELIVERED)Revision (vorher PAID): Auslöser = Sendung DELIVERED (Modul 7 §4.5, F-13)
Affiliate-Code-Rabatt = Kundenrabatt (ausgewiesen)Modul 4 (Offerten): Rabatt mindert grand_total, auf PDF sichtbar
20E-Mail/Telefon eindeutig, mit WarnungRevision: Soft-Unique — Dublette → Warnung + Merge-Vorschlag, kein stilles Duplikat
21WhatsApp volle Business-API ab StartCloud-API (kein manueller Deep-Link-MVP) — Modul 2/8 + Architektur
22GS1-Mitgliedschaft holen → SSCC aktivSSCC scharfschalten; GS1-Schweiz-Mitgliedschaft + GCP beschaffen (Marcel + Claude)
E6Box-Sortiment korrektbestätigt, kein Änderungsbedarf

7.8 Geschäftsentscheide getroffen (2026-06-28 Teil 3, Marcel)

FrageEntscheidUmsetzung
1 Anfangssaldo10'000 CHF (Default Bank; Kasse 0, sofern nicht anders)Eröffnungsbuchung/app_settings
2 DR-Preiszonenganz DR = ein Preis (keine Zonen-Staffelung)eine Default-Zone
3 MwStVersandleistung MwSt-befreit (Ausfuhr, Art. 23 MWSTG / 0 %)vat_code='EXEMPT'; System bleibt MwSt-fähig für allf. steuerbare Inland-Leistungen
4 OCRselbst gehostet (Proxmox); Anbieter/Modell TBDkein Drittland-/Cloud-Transfer fürs OCR (Compliance-Plus); Modul 2/45/20
5 Provisionfester %-Satz je Affiliate (kein Staffeln, Start)commission_kind='percent', flat
6 Provisions-Zeitpunkterst wenn ganze Sendung zugestelltDELIVERED (voll)
7 Offerten-Gültigkeit14 Tagewie gehabt
8 Offertennummerjährlich zurücksetzen (QUO-JJJJ-NNNN)Sequenz pro Jahr
9 Depot-Verfallsfrist1 Monat nach Erhalt (Zustellung)Forfeit 1 M nach DELIVERED
10 Verfallenes Depotnicht als Ertrag (finanziell neutral)keine INCOME-Buchung
11 Depot-Kardinalität1 Fass = 1 Pfand (1:1)deposit_orders.box_id 1:1
12 Reopen Periodenur Admin (Marcel)RLS
13 Auth/2FA2FA ja; IdP = Azure Entra ID und/oder SupabaseModul 8 Auth
14 Mehrfachrollenja (E1 entschieden)user_roles n:m
16 WhatsApp-Botmittelfristig ja (Phase 2+)geplant
17 Container-Auslastung führend (capacity_cbm) + Box-Anzahl ergänzendModul 5

Noch offen: → siehe §7.9 — die meisten dieser Punkte sind dort als Best-Practice-Entscheid geklärt.


7.9 Best-Practice-Entscheide (2026-06-28, autonom geklärt — „business follows best practice")

Auftrag Marcel: „kläre alle Punkte selber, hol best-practice advice, wir folgen deiner Empfehlung." Umgesetzt via vier parallele Recherche-/Fold-Agenten (Compliance/revDSG, Self-Host-OCR, CH-Finanzen, DDL-Dedup) + eigene technische Folds. Alle Defaults sind gesetzt (überstimmbar), nicht mehr offen.

#PunktBest-Practice-EntscheidFundstelle
B1Self-Host-OCR-ModellQwen3-VL-8B-Instruct (Apache 2.0) primär + PaddleOCR-VL Fallback; Proxmox LXC+GPU; interner API-Kontrakt OCR_SELFHOST_URL; Confidence-RoutingModul 2 §8–10
B2Treuhänder-Exportformat (#15)bexio-kompatibles Buchungs-CSV (KMU/Käfer-Kontenrahmen, tool-neutral: Banana/Sage/Abacus lesen es)Modul 6 §11
B3Affiliate-Provisionssatz (Wert)Default 5 % (percentage), je Affiliate/Code übersteuerbarModul 7 §10, Dok 30
B4Affiliate-IBAN-Verschlüsselung (X-5)pgsodium/Supabase Vault, Entschlüsselung nur server-seitig (ADMIN/BUCHHALTUNG)Dok 30, Modul 7
B5Datenresidenz (comp-02)Supabase EU Frankfurt (eu-central-1) + Vercel fra1 (verbindlich)Dok 45 §4.4.2, Dok 20
B6DPAs (comp-02)Zeichnungs-Checkliste: Supabase, Vercel, Resend, Meta/WhatsApp, Revolut (OCR self-hosted → entfällt)Dok 45 §4.3
B7Bearbeitungsverzeichnis (comp-06)gepflegtes Verzeichnis V1–V7 (Art. 12 revDSG) instanziiertDok 45 §5
B8DSFA (comp-08)Schwellenprüfung → DSFA erforderlich (KYC+Drittland), Befund dokumentiert; keine EDÖB-Vorabkonsultation nötigDok 45 §12
B9Incident-Runbook (comp-10)Schritt-für-Schritt Art. 24 revDSG (EDÖB-Meldung „so rasch als möglich")Dok 45 §11
B10Geld-/Satz-Domänen (data-01)domain chf numeric(12,2) + domain pct numeric(5,2); Kurse numeric(18,8)Dok 30 §0
B11Modul-10-KYC-Felder (comp-04)an Dok 30 angeglichen: cedula_number/passport_number/id_doc_type statt id_type/id_number; Bucket kycModul 1
B12DDL-Dedup (X-3)Module 5 + 6 reproduzieren keine Voll-DDL mehr → Verweise auf Dok 30 (Single Source of Truth)Modul 5/6
B13Security-Header/CSP (sec-06)cajaspec-Site liefert CSP + HSTS + X-Frame-Options/nosniff/Referrer-Policydeploy.mjs
B14CI (repo-02/03)GitHub-Actions-Job cajaspec (typecheck+build) + engines: node>=20.github/workflows/cajaspec.yml
B15Umsetzungsplan4-Wochen-Plan bis Go-Live 25.07.2026 + Mermaid-TimelineDok 40

Verbleibend (nur extern/menschlich, nicht durch Best Practice ersetzbar): Datenschutzerklärung (Dok 46) anwaltlich abnehmen · DPAs physisch zeichnen · Supabase-Region beim realen Projekt-Setup auf EU setzen (operativ). Kleinere P2/P3-Politur (z. B. data-06/09/10, ai-03/05) bleibt optionaler Feinschliff.


Completeness-Critic-Review · Dok 99 · 2026-06-26 · Dominicano Express GmbH · Caja-Pflichtenheft. Bezug: Module 10–18, 20 (Architektur), 30 (Kanonisches Schema, normativ bei Schema-Konflikten), 40 (Roadmap), 45 (Compliance).