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)
| # | Befund | Schwere | Betroffen | Typ |
|---|---|---|---|---|
| F-01 | ✅ RESOLVED — order_lines/order_items gestrichen; Boxen werden aus quote_lines materialisiert | 🔴 Blocker | 4, 5, 3, 30 | Fehlende Entität |
| F-02 | ✅ RESOLVED — creditor_invoice/creditor_payment entfernt; Kreditoren manuell | 🔴 Blocker | 6, 30 | Fehlende Entität / tote Kette |
| F-03 | ✅ RESOLVED — deposit_payments.payment_method_id jetzt text | 🔴 Blocker | 30 | DDL-Bug |
| F-04 | ✅ RESOLVED — Ledger-Trigger setzt invoices.status=PAID/PARTIAL; Affiliate-Trigger hängt daran | 🔴 Blocker | 7, 6, 30 | Tote Auto-Posting-Kette |
| F-05 | ✅ RESOLVED — überall commission_payout (K-05); affiliate_payout gestrichen | 🔴 Blocker | 6, 20, 30 | Residual-Widerspruch |
| F-06 | ✅ RESOLVED — Forderung nur bei invoice_issued→INVOICE; quote_accepted→RECEIVABLE entfernt | 🟠 Hoch | 4, 6, 30 | Unklare Auto-Posting-Kette |
| F-07 | ✅ RESOLVED — recipients.preferred_lang (Default 'es') ergänzt; SHIPMENT_STATUS zieht Empfänger-Locale daraus | 🟠 Hoch | 5, 8, 30 | Fehlende Verbindung |
| F-08 | ✅ RESOLVED — Phantom boxes.recipient_contact_id entfernt; KYC-Gate über shipments.recipient_id → recipients | 🟠 Hoch | 2, 5, 45 | Phantom-Spalte |
| F-09 | ✅ RESOLVED — Rollen-Codes überall UPPERCASE via has_role(); caja_role-ENUM/auth_role() gestrichen | 🟠 Hoch | 6, 18, 8, 30 | Residual-Widerspruch |
| F-10 | ✅ RESOLVED — einzige Quelle app_users.preferred_locale+theme; user_preferences gestrichen | 🟠 Hoch | 18, 8, 30 | Doppelte Entität |
| F-11 | ✅ RESOLVED — i18n_labels durch locale_strings + Inline-label_* ersetzt (Affiliate/Design) | 🟡 Mittel | 1, 7, 18, 30 | Residual-Widerspruch |
| F-12 | ✅ RESOLVED — Konvention: Lookup-*_statuses=UPPERCASE, Inline-CHECK-status (affiliate/commission/closing)=lowercase; in Dok 30 §1 normativ dokumentiert | 🟡 Mittel | 4, 6, 7, 30 | Inkonsistenz |
| F-13 | ✅ RESOLVED — Lifecycle: Entstehung@invoices→PAID, Freigabe@Prüfung (DELIVERED-Voraussetzung), Auszahlung@Payout; in 16+30 fixiert | 🟡 Mittel | 7, 5, 30 | Unklare Kette |
| F-14 | ✅ RESOLVED — deposit_orders.movement_id (Soll) + AP-4 payment_links (Abono→Soll) via deposit_order_id, kein reference-Match | 🟡 Mittel | 5, 6, 30 | Doppelte Buchungslogik |
| F-15 | ✅ RESOLVED — lead_source als text+CHECK (5 Werte) statt Postgres-ENUM, Validierung erhalten (K-20); 30+1 angeglichen | 🟡 Mittel | 1, 30 | Residual-Widerspruch |
| F-16 | ✅ RESOLVED — E.164-CHECK für phone_secondary + whatsapp_number ergänzt (30+1) | 🔵 Niedrig | 1, 30 | Validierungslücke |
| F-17 | ✅ RESOLVED — Mobile-Patterns (Bottom-Sheet/Karten) für Treuhänder-Export, MWST-Settings, Container-Bulk, Affiliate-Mini-Portal in Modul 18 §5.7 ergänzt | 🟡 Mittel | 6, 7, 8, 18 | Fehlendes Mobile-Pattern |
| F-18 | ✅ RESOLVED — vat_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 | 🟡 Mittel | 6, 30 | Unklare Mechanik |
| F-19 | ✅ RESOLVED — Modul-Refs ergänzt: alle Lookups folgen zentraler RLS-Policy Dok 30 §13 (11+14) | 🔵 Niedrig | 2, 5 | Fehlende RLS (Modul-Ebene) |
| F-20 | ✅ RESOLVED — notifications.owner_type-CHECK ergänzt (inkl. recipient/shipment); storage_objects um recipient erweitert (30+17) | 🔵 Niedrig | 8, 30 | Enum-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 | 🟡 Mittel | 18, 2, 45 | Widerspruch Datenhaltung |
| F-22 | ✅ RESOLVED — Einzige Quelle box_products.is_returnable; boxes.is_returnable abgeleitet (bei Materialisierung, kein Default); 12+14+30 | 🔵 Niedrig | 3, 5, 30 | Redundanz |
| F-23 | ✅ RESOLVED — WhatsApp-Matching auf whatsapp_number mit Fallback phone_primary (K-03); contacts.phone-Wording in Modul 2 korrigiert | 🔵 Niedrig | 2, 30 | Residual-Widerspruch |
| F-24 | ✅ RESOLVED — Offerten-Empfänger = Freitext (vor KYC, PDF); strukturierter Empfänger erst bei Sendung (shipments.recipient_id); Übergang in 13/14/30 dokumentiert | 🟡 Mittel | 4, 5, 30 | Fehlende 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_linesder konvertierten Offerte materialisiert (orders.quote_id+ Produkt-Snapshot viaboxes.box_product_id).order_lines/order_itemsgestrichen in Modul 4 §4.4/§6.3 und Modul 3 §2/§6.2; Dok 30 §2.6 mit Hinweis ergänzt;invoice_linesals 🔲 Zukunft (MWST je Position).
Fund:
- Offerten §4.4(b): „Neuen Auftrag anlegen:
orders.INSERT… Kopie der Positionen alsorder_lines". - Offerten §6.3: „
order_lineswerden ausquote_lineskopiert (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 —
ordersist dabei,order_lines/order_itemsfehlt 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:
- Entscheiden, ob Auftragspositionen überhaupt eine eigene Tabelle brauchen oder ob
boxes(die ohnehin pro Position N-fach entstehen) + derquote_id-Rückbezug genügen. Da Modul 5 die Boxen direkt aus den Offertenpositionen materialisiert undorders.total_chfnur ein read-only-Spiegel ist, ist eine eigeneorder_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). - Falls doch nötig (z. B. für Rechnungspositionen/MWST je Position): kanonische Tabelle
order_linesin Dok 30 definieren (analogquote_lines:order_id,position,product_id,product_snapshot,price_list_item_id,unit_price_chf,quantity, Rabattfelder,line_total_chf) undorder_itemsals Jargon streichen. - Konsequenz prüfen für
invoices: hat eine Rechnung Positionen? Aktuell istinvoiceskopflos (nurtotal_chf) — bei aktiver MWST je Satz/Position bräuchte esinvoice_lines.
F-02 — creditor_invoice / creditor_payment: source_type ohne Quell-Tabelle
✅ RESOLVED (2026-06-27): Kreditoren bleiben manuell.
'creditor_invoice'/'creditor_payment'aus dersource_type-Union entfernt (Dok 30 §4.1 + K-05, Modul 6 §3.2); AP-7/AP-8 als „entfällt" markiert. Lieferantenrechnungen/-zahlungen sind manuellemovements(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_paymentoder „Lieferantenrechnung" in irgendeinem Schema. Kreditoren entstehen heute ausschliesslich als manuellemovementsvom TypEXPENSE/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_paymentaus dersource_type-Union entfernen; AP-7/AP-8 streichen; Lieferantenrechnung/-zahlung sind manuellemovements(source_type='manual'). Das ist der kleinste Eingriff. - (B) Echte Kreditoren-Entität einführen (
supplier_invoices+supplier_paymentsanaloginvoices/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_idvonuuidauftext references payment_methods(code)korrigiert (Dok 30 §5) — passt jetzt zumtext-PK und zudeposit_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_id → payment_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/movementsführtinvoices.statusnach:Σ zugeordnete Zahlungen ≥ total_chf→PAID,>0 <total→PARTIAL, sonstOPEN(Dok 30 §2.7, Modul 6 §4 Workflow C — bewusste Ausnahme von „Status nur aus Views"). Der Affiliate-Provisions-Trigger hängt nicht anPAID, sondern an SendungDELIVERED(Entscheid #19, 2026-06-28; Modul 16 §4.5);invoices.statusdient Mahnwesen/Debitoren.
Fund:
- Affiliate §4.5 + §6 (Events): „
invoice.status → 'paid'└─► IF order.affiliate_id IS NOT NULL └─► INSERTcommission_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 aufinvoices.status. Dok 30 §2.7 sagt explizit: „Restschuld wird nicht aufinvoicesgespeichert, sondern aus dem Ledger abgeleitet".invoices.status(DefaultOPEN) wird damit von niemandem aufPAIDgesetzt. - Zusätzlich Schreibweise: Affiliate nutzt
'paid'(lowercase),invoice_statusesdefiniertPAID(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:
- 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 = 0für diese Rechnung), erste Teilzahlung, oder SendungDELIVERED. - Wenn an „Rechnung bezahlt" festgehalten wird: Es braucht einen expliziten Mechanismus, der
invoices.statusaus dem Ledger nachführt (Trigger aufpayment_links/movements, der bei Voll-Tilgung der zugehörigenINVOICE-Bewegunginvoices.status='PAID'setzt) — sonst bleibtinvoices.statusein totes Feld. Das ist eine bewusste Design-Entscheidung gegen die „Status nur aus Views"-Regel und muss benannt werden. - 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_payoutgestrichen in Modul 6 §2/§3.2 und Architektur §8.1 (PostMovementInput) / §8.2 (Event-Tabelle) + §3-Narrativ. Affiliate §6 nutzte bereitscommission_payout.
Fund:
- Dok 30 K-05 (Zeile 19) und §4.1 (Zeile 571) definieren kanonisch
'commission_payout'in dersource_type-Union — ohneaffiliate_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): durchgehendaffiliate_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 inv_debitoren/v_dashboardmehr.
Fund:
- AP-10 (Dok 30 §11.1): Offerte angenommen →
source_type='quote_accepted'→RECEIVABLE(optional) mittotal=grand_total_chf, treibtopen_debt(Forderung). - AP-1: Rechnung gestellt →
source_type='invoice_issued'→INVOICEmittotal=invoices.total_chf(=quotes.grand_total_chf, Dok 30 §2.7), treibt ebenfallsopen_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_dashboardsummierenopen_debt_chfüberINVOICEundRECEIVABLE.
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_accepted→RECEIVABLEundinvoice_issued→INVOICEeinander ausschliessen: Entweder gibt es eine Vormerkung VOR Rechnungsstellung (selten relevant, da Konversion sofort fakturiert) oder direkt dieINVOICE. Empfehlung: AP-10 streichen (kein Mehrwert, da bei Konversion sofortINVOICEentsteht) oder explizit auf „nur wenn Auftrag ohne sofortige Rechnung" einschränken — mit Guard gegen gleichzeitigeINVOICEderselbenreference+contact_id. - Falls beide bestehen bleiben:
v_debitoren/v_dashboardmüssen Doppelzählung verhindern (z. B.RECEIVABLEignorieren, sobald eineINVOICEmit gleicherreferenceexistiert).
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 auscontacts.preferred_lang); Default ES deckt den Normalfall ab. Fehlende Telefonnummer →status='skipped'(Plattform §4.6) bleibt abgedeckt.
Fund:
- Plattform §6.3: Template
SHIPMENT_STATUSgeht an „Kunde + Empfänger-DR";notifications.localeistNOT NULL CHECK in ('de','es','en')und „= Empfänger-Sprache". recipients(Dok 30 §2.3) hat keinpreferred_lang/locale-Feld. Empfänger sind keinecontactsund keineapp_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:
recipients.preferred_lang text default 'es' check (...)ergänzen (Dok 30 §2.3) oder explizit festlegen: Empfänger-DR-Notifications sind immer ES (dann brauchtenqueue_notificationfür diesen Pfad einen Fix-Wert).- Opt-in/Telefon-Verfügbarkeit klären:
recipients.phone_primaryist nullable → bei fehlender Nummer greiftstatus='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 → recipientsfixiert; KYC-Gate prüftrecipients.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.boxeshat keinerecipient_contact_id-Spalte (war reine Doku-Phantomspalte, nie in einer DDL).
Fund:
- WhatsApp/OCR §6 (Zeile 344): „
boxes.recipient_contact_id→contacts.kyc_statusmussVERIFIEDsein". - 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 Spalterecipient_contact_id. Der Empfänger hängt anshipments.recipient_id → recipients, undrecipients.contact_id → contacts(optional). Eine Box kennt ihren Empfänger nur indirekt übershipment_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:
- Die KYC-Gate-Bedingung auf die reale Kette umformulieren:
shipments.recipient_id → recipients.contact_id → contacts.kyc_status. Den Fallrecipients.contact_id IS NULLdefinieren (Empfänger ohne Golden-Record — ist KYC dann amrecipients-Satz selbst zu prüfen?recipients.cedula_numberexistiert). - Modul 2 §6 und Compliance §4.1 entsprechend korrigieren (kein
boxes.recipient_contact_id). - Grundsatzfrage: Soll KYC am Empfänger (
recipients) oder am verknüpften Kontakt hängen?recipientshat eigenecedula_number/cedula_scan_path, aber keinkyc_status-Feld → evtl. fehltrecipients.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.3caja_role-ENUM gestrichen (→roles-TEXT-Lookup); Modul 6 §3.7auth_role()/Personennamen →has_role('CODE'); Modul 16 RLS-Bullets auf UPPERCASE. Mappingadmin_office→BUCHHALTUNG,operations→OPERATIONS,driver→FAHRERdokumentiert.
Fund:
- Modul 8 / Dok 30 (kanonisch, K-17):
ADMIN, BUCHHALTUNG, OPERATIONS, FAHRER, AFFILIATE, READONLY(UPPERCASE), RLS viahas_role('CODE'). - Modul 18 §3.3:
CREATE TYPE caja_role AS ENUM ('admin','operations','admin_office','driver','affiliate','readonly')— lowercase, andere Namen (admin_officestattBUCHHALTUNG,operations/driverstattOPERATIONS/FAHRER), und als Postgres-ENUM (widerspricht K-20: erweiterbare Mengen = TEXT-Lookup). - Modul 6 §3.7: RLS über Personennamen
marcel/mariela/markus/arkys+ Helperauth_role()(statthas_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:
- Modul 18 §3.3:
caja_role-ENUM streichen (Rollen sindroles-Lookup mit TEXT-Code). - Modul 6 §3.7: alle
auth_role()-Policies aufhas_role('ADMIN'|'BUCHHALTUNG'|...)umschreiben;marcel/mariela/markus/arkyssind nur Referenz. admin_office→BUCHHALTUNGmappen (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'zuapp_usersergänzt (Dok 30 §6.1);user_preferences(Modul 18 §3.2) gestrichen; Workflows §4.1/§4.2 + Sprachdeklaration aufapp_users.preferred_locale/themeumgeschrieben. 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 Defaultlang='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 inapp_usersintegrieren,user_preferencesfä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_labelswird nicht gebaut (K-16). Modul 18 §3.1CREATE TABLE i18n_labels→locale_strings-Doku + Hybrid; Modul 16 §3.7INSERT INTO i18n_labels→INSERT INTO locale_strings (namespace,key,locale,value); Modul 13 Status-Lookup-Hinweis auf Inline-label_*/locale_strings. (Modul 1 nutztcontact_role_labels, keini18n_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 pendingbeiinvoices.status → PAID(Ledger-Trigger, konsistent mit F-04 — einziger Auslöser); (b) Freigabe→ approvedbei der Abrechnungsprüfung (SendungDELIVEREDist fachliche Voraussetzung, kein DB-Trigger); (c) Auszahlung→ paidbeim 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 erzeugtpayment_links(payment_id=Abono, target_id=deposit_orders.movement_id, amount); der Soll wird überdeposit_payments.deposit_order_id → deposit_orders.movement_ideindeutig gefunden (analog AP-2/INVOICE). So finden Soll+Abono inv_anzahlungensicher zusammen. Fixiert in Dok 30 §11.1, Modul 14 §4.5, Modul 15 §4 Workflow B.
Fund: AP-3 bucht deposit_order→DEPOSIT_CASH (Soll, total=deposit_total_chf, paid=0); AP-4 bucht deposit_payment→DEPOSIT_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_sourceist einetext-Spalte mitCHECK 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) liestvat_rates.rate_pctzumvat_codeund schreibtvat_amount_chfals persistierte Spalte auf die Quell-Zeile (movements/invoices/quotes). Keine View und keine generated column (darate_pctin 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 nurkyc/documents/receipts/public-assets); Rohbild liegt im privatenkyc-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 zurrecipients-Zeile. Übergang fixiert in Dok 30 §2.5, Modul 13 §3.x, Modul 14. 🔲 Optionalequotes.recipient_id → recipientsbleibt 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_secondaryundcontacts.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 (CRUfür ADMIN,Rfü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) umrecipienterweitert (war in Dok 30 bereits enthalten). Damit kannSHIPMENT_STATUSan 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_returnableist abgeleitet: wird bei der Box-Materialisierung (Modul 14 §4.1) ausbox_products.is_returnableinitialisiert; der widersprüchlicheboxes-Defaulttruewurde 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_numbermit Fallbackcontacts.phone_primary(contacts.phoneexistiert kanonisch nicht). Konsistent mit Dok 30 K-03 und derchk_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_usersvs.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_logvia 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:
- ✅ F-05, F-11, F-09 (erledigt 2026-06-27): Modul-Specs 6/16/18 auf Dok 30 nachgezogen (
commission_payout;locale_stringsstatti18n_labels; UPPERCASE-Rollen viahas_role()stattcaja_role/auth_role()). - ✅ F-03 (erledigt 2026-06-27):
deposit_payments.payment_method_idauftext references payment_methods(code)korrigiert (Dok 30 §5). - ✅ F-01, F-02, F-04 (erledigt 2026-06-27): keine
order_lines(Box-Materialisierung ausquote_lines); Kreditoren manuell; Invoice-PAIDvia Ledger-Trigger. - ✅ F-06, F-14 (erledigt 2026-06-27, Finanz-Korrektheit): Doppelzählung
RECEIVABLE/INVOICEdurch Streichung von AP-10 ausgeschlossen; Depot-Soll/Abono-Verknüpfung viadeposit_orders.movement_id+payment_links(F-14) fixiert. - ✅ F-07, F-08, F-24 (erledigt 2026-06-27, Empfänger-Kette):
recipients.preferred_lang; KYC-Gate auf reale Join-Ketteshipments.recipient_id → recipients; Offerten-Freitext vs. strukturierter Sendungs-Empfänger dokumentiert. - ✅ 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.
- ✅ 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. - 🔲 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
| # | Entscheid | Wahl | Umgesetzt in |
|---|---|---|---|
| E7 | Tarifmodell | Hybrid — 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) |
| E4 | Personen-Stand auf Beleg | Einfrieren (Snapshot) — zoll-/revisionssicher, überlebt Kontakt-Merge. | Dok 30 §2.7/§7 (bill_to_snapshot, sender_snapshot, recipient_snapshot) |
| E8 | Endkunden-Portal | Phase 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
| Gap | IST-Regel | Anforderung | Status |
|---|---|---|---|
| G1 | BR-30 | Personen-Snapshot (Absender/Empfänger) auf Sendung+Rechnung einfrieren | ✅ gefoldet (Dok 30 §2.7/§7) |
| G2 | BR-39/40/41 | Zoll-/Aduana-Belege + Sendungsliste + Manifest-View | ✅ gefoldet (Modul 14 §10.1, doc_class erweitert) |
| G3 | BR-20 | Zweiter Wiegepunkt weight_rd_kg (RD) | ✅ gefoldet (Dok 30 §7, Modul 14 §10.2) |
| G4 | BR-48 | Zollfelder (Documento Embarque = containers.bl_number; Dokumenttyp Absender) | 🟡 teilw. (BL vorhanden; Absender-Dokumenttyp im Manifest §10.1) |
| G5 | BR-33/05 | 3-Fahrer-Zuordnung + Fahrer-RLS BR-05 | ✅ gefoldet (Dok 30 §7, Modul 14 §10.4) — Entscheid E10 offen |
| G6 | BR-28 | Unterschrift-Liefernachweis (signer_name + doc_class='signature') | ✅ gefoldet (Dok 30 §7, Modul 14 §10.3) — Pflicht/optional E11 offen |
| G7 | BR-23 | Volumen-Tarif für Freimaß neben Box-Festpreisen | ✅ gefoldet (E7-Hybrid, Dok 30 §3.3) |
| G8 | BR-S12 | CSRF-/Origin-Schutz explizit | ✅ gefoldet (Modul 17 §10.1) |
| G9 | BR-S07 | Application-Error-Logging (separat vom audit_log) | ✅ gefoldet (Modul 17 §10.3) |
| G10 | BR-08/12 | Status-Mapping 10→9 + Datums-Semantik je Status (View) | ✅ gefoldet (Modul 14 §10.5) — DR_DEPOT Entscheid E3 ✅ (2026-06-28) |
| G-Set | BR-S08 | app_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: getrennt — app_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=truesobald 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):
| Bereich | IST-Feature | Vertreten in |
|---|---|---|
| Logistik | Notification-Trigger-Matrix (BR-15, inkl. Zahlungs-Gate) | Modul 14 §11.1 |
| Logistik | Bulk-Statuswechsel mit History+Mail (BR-16) | Modul 14 §11.2 |
| Logistik | Box-Stammdaten-Sperre = IsSent (BR-22/26) | Modul 14 §11.3 |
| Logistik | Fahrer-Picker + Fahrer-RLS (BR-33/05) | Modul 14 §11.4, Dok 30 §13 Fn 12 |
| Logistik | Status-Zähl-Badges (BR-38/43) | Modul 14 §11.6 (v_tracking_status_counts) |
| Logistik | Datumsfilter inkl. + Sortierung (BR-35/37/44) | Modul 14 §11.7 |
| Logistik | Report-Export-Modi + Belegkopf + Absender-Doktyp (BR-40/41/48) | Modul 14 §11.8, Dok 30 §2.1 id_doc_type |
| Offerten | Preis-Override-Recht serverseitig (BR-25) | Modul 13 (Nachtrag) |
| CRM | alias-Feld (BR-P16) | Dok 30 §2.1/§2.3 |
| CRM | Empfänger→Kunde-Promotion (BR-P15) | Modul 10 (Nachtrag), recipients.promoted_contact_id |
| CRM | E-Mail-Dedup-Doku, Telefon-Mapping (BR-P03/P16) | Modul 10 (Nachtrag), Dok 30 §2.1 |
| Plattform | Settings-Screen (IST Settings.html) | Modul 17 §11.1 |
| Plattform | Rollen-/Rechte-Matrix read-only (IST Roles.html) | Modul 17 §11.2 |
| Plattform | Objekt-Verlauf-Tab (IST „Historial de cambios") | Modul 17 §11.3, Modul 10 |
| Plattform | Logout / 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)
| # | Entscheid | Umsetzung |
|---|---|---|
| 1 | MwSt ab sofort aktiv | app_settings.vat_enabled=true; Modul 6 MwSt scharf |
| 2 | Preise inkl. MwSt (brutto) | vat_method='inclusive'; MwSt wird aus Bruttopreis herausgerechnet |
| 3 | Mehrwährung CHF + DOP; DOP-Zahlung via Revolut Business | currency/fx_rate im Ledger; neue Integration Revolut Business (Modul 6 + Architektur) |
| 4 | Anfangssaldo Kasse/Bank | 🔲 Betrag folgt von Marcel |
| 5 | KYC-Aufbewahrung: max. 24 Monate nach letzter Interaktion | retain_until = last_interaction + 24M; Auto-Löschung |
| 6 | Drittland-/OCR-Übertragung erlaubt (Einwilligung im Onboarding-Vertrag) | Onboarding-Flow erfasst/referenziert Consent (Modul 2 + 45) |
| 7 | Datenschutzerklärung (CH/revDSG): ja | Entwurf 46-datenschutz.md (juristisch zu prüfen) |
| 8 | Aufbewahrung CH-Recht (OR 957/GeBüV): ja | 10 J. Belege; in Modul 45 verbindlich |
| 9 | DR-Lager: ja | neuer Tracking-Status DR_DEPOT (zw. DR_CUSTOMS und DELIVERED) |
| 10 | Keine RD-Nachwiegung | boxes.weight_rd_kg nicht aktiv genutzt (Feld bleibt optional/deprecated) |
| 11 | 3 Fahrer-Rollen: ja; alle Fahrer bedienen die App | FK auf app_users (kein Fahrer ohne Login) — E10/E10b gelöst |
| 12 | Liefernachweis: Empfänger wählt | Methode je Zustellung: Unterschrift · Ablage/Deponierung mit Foto (CH-Post-Style Abstellerlaubnis) · Selbstdeklaration |
| 13 | Bild pro Box-Typ: ja (Bilder extern) | box_products.image_path; Bilder TBD (nicht von Marcel) |
| 14 | DR-Preiszonen | 🔲 folgt von Marcel |
| 15 | Rundung: immer aufrunden auf ganze Zahlen | Preis-Resolver ceil auf ganze CHF |
7.7 Geschäftsentscheide getroffen (2026-06-28 Teil 2, Marcel)
| # | Entscheid | Umsetzung |
|---|---|---|
| 16 | Status-Nachrichten an beide (CH-Absender + DR-Empfänger) | Notification-Matrix (Modul 14 §11.1, Modul 17 §6) |
| 17 | Keine Unterdrückung bei offener Rechnung | Revision: Zahlungs-Gate entfernt — Status-Nachrichten gehen immer |
| 18 | Provision = % je Affiliate/Sublieferant | referral_codes/commission_entries commission_kind='percent', Satz je Affiliate konfigurierbar |
| 19 | Provision 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 |
| 20 | E-Mail/Telefon eindeutig, mit Warnung | Revision: Soft-Unique — Dublette → Warnung + Merge-Vorschlag, kein stilles Duplikat |
| 21 | WhatsApp volle Business-API ab Start | Cloud-API (kein manueller Deep-Link-MVP) — Modul 2/8 + Architektur |
| 22 | GS1-Mitgliedschaft holen → SSCC aktiv | SSCC scharfschalten; GS1-Schweiz-Mitgliedschaft + GCP beschaffen (Marcel + Claude) |
| E6 | Box-Sortiment korrekt | bestätigt, kein Änderungsbedarf |
7.8 Geschäftsentscheide getroffen (2026-06-28 Teil 3, Marcel)
| Frage | Entscheid | Umsetzung |
|---|---|---|
| 1 Anfangssaldo | 10'000 CHF (Default Bank; Kasse 0, sofern nicht anders) | Eröffnungsbuchung/app_settings |
| 2 DR-Preiszonen | ganz DR = ein Preis (keine Zonen-Staffelung) | eine Default-Zone |
| 3 MwSt | Versandleistung MwSt-befreit (Ausfuhr, Art. 23 MWSTG / 0 %) | vat_code='EXEMPT'; System bleibt MwSt-fähig für allf. steuerbare Inland-Leistungen |
| 4 OCR | selbst gehostet (Proxmox); Anbieter/Modell TBD | kein Drittland-/Cloud-Transfer fürs OCR (Compliance-Plus); Modul 2/45/20 |
| 5 Provision | fester %-Satz je Affiliate (kein Staffeln, Start) | commission_kind='percent', flat |
| 6 Provisions-Zeitpunkt | erst wenn ganze Sendung zugestellt | DELIVERED (voll) |
| 7 Offerten-Gültigkeit | 14 Tage | wie gehabt |
| 8 Offertennummer | jährlich zurücksetzen (QUO-JJJJ-NNNN) | Sequenz pro Jahr |
| 9 Depot-Verfallsfrist | 1 Monat nach Erhalt (Zustellung) | Forfeit 1 M nach DELIVERED |
| 10 Verfallenes Depot | nicht als Ertrag (finanziell neutral) | keine INCOME-Buchung |
| 11 Depot-Kardinalität | 1 Fass = 1 Pfand (1:1) | deposit_orders.box_id 1:1 |
| 12 Reopen Periode | nur Admin (Marcel) | RLS |
| 13 Auth/2FA | 2FA ja; IdP = Azure Entra ID und/oder Supabase | Modul 8 Auth |
| 14 Mehrfachrollen | ja (E1 entschieden) | user_roles n:m |
| 16 WhatsApp-Bot | mittelfristig ja (Phase 2+) | geplant |
| 17 Container-Auslastung | m³ führend (capacity_cbm) + Box-Anzahl ergänzend | Modul 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.
| # | Punkt | Best-Practice-Entscheid | Fundstelle |
|---|---|---|---|
| B1 | Self-Host-OCR-Modell | Qwen3-VL-8B-Instruct (Apache 2.0) primär + PaddleOCR-VL Fallback; Proxmox LXC+GPU; interner API-Kontrakt OCR_SELFHOST_URL; Confidence-Routing | Modul 2 §8–10 |
| B2 | Treuhänder-Exportformat (#15) | bexio-kompatibles Buchungs-CSV (KMU/Käfer-Kontenrahmen, tool-neutral: Banana/Sage/Abacus lesen es) | Modul 6 §11 |
| B3 | Affiliate-Provisionssatz (Wert) | Default 5 % (percentage), je Affiliate/Code übersteuerbar | Modul 7 §10, Dok 30 |
| B4 | Affiliate-IBAN-Verschlüsselung (X-5) | pgsodium/Supabase Vault, Entschlüsselung nur server-seitig (ADMIN/BUCHHALTUNG) | Dok 30, Modul 7 |
| B5 | Datenresidenz (comp-02) | Supabase EU Frankfurt (eu-central-1) + Vercel fra1 (verbindlich) | Dok 45 §4.4.2, Dok 20 |
| B6 | DPAs (comp-02) | Zeichnungs-Checkliste: Supabase, Vercel, Resend, Meta/WhatsApp, Revolut (OCR self-hosted → entfällt) | Dok 45 §4.3 |
| B7 | Bearbeitungsverzeichnis (comp-06) | gepflegtes Verzeichnis V1–V7 (Art. 12 revDSG) instanziiert | Dok 45 §5 |
| B8 | DSFA (comp-08) | Schwellenprüfung → DSFA erforderlich (KYC+Drittland), Befund dokumentiert; keine EDÖB-Vorabkonsultation nötig | Dok 45 §12 |
| B9 | Incident-Runbook (comp-10) | Schritt-für-Schritt Art. 24 revDSG (EDÖB-Meldung „so rasch als möglich") | Dok 45 §11 |
| B10 | Geld-/Satz-Domänen (data-01) | domain chf numeric(12,2) + domain pct numeric(5,2); Kurse numeric(18,8) | Dok 30 §0 |
| B11 | Modul-10-KYC-Felder (comp-04) | an Dok 30 angeglichen: cedula_number/passport_number/id_doc_type statt id_type/id_number; Bucket kyc | Modul 1 |
| B12 | DDL-Dedup (X-3) | Module 5 + 6 reproduzieren keine Voll-DDL mehr → Verweise auf Dok 30 (Single Source of Truth) | Modul 5/6 |
| B13 | Security-Header/CSP (sec-06) | cajaspec-Site liefert CSP + HSTS + X-Frame-Options/nosniff/Referrer-Policy | deploy.mjs |
| B14 | CI (repo-02/03) | GitHub-Actions-Job cajaspec (typecheck+build) + engines: node>=20 | .github/workflows/cajaspec.yml |
| B15 | Umsetzungsplan | 4-Wochen-Plan bis Go-Live 25.07.2026 + Mermaid-Timeline | Dok 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).