User Stories — Epic 1: CRM / Kundenstamm

Format wie Pilot Epic 5. Methodik: Connextra (Als/möchte/damit) + Gherkin-Akzeptanzkriterien (Gegeben/Wenn/Dann) + INVEST-Qualitätsgate. Sprache der Stories DE; Fachbegriffe/Status wie im Schema. Bezug: Modul 1 (10-crm.md), Schema Dok 30 §2.1–§2.4, RLS Dok 30 §13. Rollen-Codes kanonisch (UPPERCASE): ADMIN, BUCHHALTUNG, OPERATIONS, READONLY (+ FAHRER/AFFILIATE nur als Lese-Negativfall). IDs: US-CRM-NN (stabil). Status: Entwurf (zur Freigabe).


Personas in diesem Epic

RollePersonBezug zum CRM
ADMINMarcel/Marielaalles — anlegen, bearbeiten, mergen, löschen/anonymisieren
BUCHHALTUNGMarielaKontakte lesen + anlegen/bearbeiten (kein Merge-Delete, kein Rollen-Schreiben)
OPERATIONSMarkusKontakte/Empfänger/Rollen anlegen + bearbeiten, Dedup auflösen (kein Delete)
READONLYTreuhänder/Gastnur lesen — maskiert (kein cedula_number, kein Ausweis-Scan)
Lead/Interessent— (kein Login)Datensubjekt: kommt via Website-Formular/WhatsApp in contacts (Rolle lead)
Empfänger-DR— (kein Login)Datensubjekt: recipients-Zeile, verknüpft mit Absender-Kontakt

INVEST-Gate (für alle Stories dieses Epics geprüft): jede Story ist unabhängig schneidbar, verhandelbar, liefert Geschäftswert, schätzbar, klein genug für einen Schritt und über die Gherkin-Szenarien testbar (inkl. Negativfälle/RLS).


US-CRM-01 — Lead-Intake aus Website-Formular

Als Interessent (kein Login) möchte ich, dass meine Website-Anfrage automatisch als Lead landet, damit Caja sich ohne manuelle Erfassung bei mir meldet.

Akzeptanzkriterien

  • Gegeben ein gültiger POST auf /api/send-contact (Honeypot _honey leer), wenn kein Treffer mit Score ≥ 0.85 existiert, dann wird ein contacts-Datensatz mit status='lead', lead_source='website_form' und eine contact_roles-Zeile role='lead' angelegt + ein audit_log-Eintrag contact.created geschrieben.
  • Gegeben ein Treffer mit Score ≥ 0.85 (Telefon exakt bzw. Name+E-Mail), wenn das Formular eingeht, dann wird der bestehende Datensatz aktualisiert (updated_at) statt ein Duplikat anzulegen — Audit contact.updated_from_lead.
  • Gegeben _honey ist nicht leer (Bot), wenn der Request eingeht, dann passiert keine Datenänderung und die Antwort ist 200 ohne Aktion.
  • Gegeben phone fehlt/ungültig, wenn der Lead anlegt wird, dann bleibt phone_primary=NULL und needs_verification=true (Lead trotzdem erstellt); email ist optional.
  • Gegeben type='Programa Afiliación', wenn der Lead anlegt wird, dann wird zusätzlich contact_roles role='affiliate' gesetzt.

Traceability: contacts, contact_roles (§2.1/§2.2), Workflow §4.1; audit_log (§6.1).


US-CRM-02 — Lead-Schnellerfassung aus WhatsApp

Als OPERATIONS möchte ich einen eingehenden WhatsApp-Lead in unter 30 Sekunden erfassen, damit keine Anfrage verloren geht.

Akzeptanzkriterien

  • Gegeben eine WhatsApp-Absendernummer, wenn ich „Lead aus WhatsApp" öffne, dann ist die Telefonnummer (E.164) vorausgefüllt und ich ergänze nur Vorname*, Nachname*, Sprache.
  • Gegeben ich verlasse das Telefonfeld (on-blur), wenn bereits ein Kontakt mit dieser Nummer existiert, dann erscheint ein Hinweis „Kontakt bereits vorhanden" mit Link zum bestehenden Datensatz (kein stiller Doppelanlage).
  • Gegeben ich speichere, dann wird lead_source='whatsapp' gesetzt, role='lead' angelegt und ein audit_log-Eintrag geschrieben.

Traceability: contacts.lead_source (§2.1), Workflow §4.2; whatsapp_inbound (§6.4).


US-CRM-03 — Kontakt manuell anlegen

Als OPERATIONS möchte ich einen Kontakt manuell anlegen, damit auch telefonische/persönliche Kontakte sauber im Stamm landen.

Akzeptanzkriterien

  • Gegeben das Formular „Neuer Kontakt", wenn ich speichere, dann sind Vorname* und Nachname* Pflicht; preferred_lang Default es; lead_source='manual'.
  • Gegeben ich wähle die Rolle customer aus, wenn ich speichere, dann wird status='active' gesetzt (statt lead).
  • Gegeben eine ungültige Telefonnummer (verletzt E.164 ^\+[1-9]\d{6,14}$), wenn ich speichere, dann wird die Eingabe abgelehnt (chk_phone_primary_e164); gleiches gilt für phone_secondary/whatsapp_number.
  • Gegeben mögliche Duplikate (Score ≥ 0.75), wenn ich speichere, dann erscheint eine Duplikat-Warnung mit Score + Übereinstimmungsfeldern und der Wahl „Trotzdem anlegen" | „Bestehenden öffnen".

Traceability: contacts + E.164-CHECKs (§2.1), Workflow §4.3, Validierungen CRM §7.


US-CRM-04 — Kontakt bearbeiten (Stammdatenpflege)

Als OPERATIONS/BUCHHALTUNG möchte ich Kontaktdaten bearbeiten, damit Stammdaten aktuell und korrekt bleiben.

Akzeptanzkriterien

  • Gegeben ein bestehender Kontakt, wenn ich Felder ändere und speichere, dann werden updated_at/updated_by gesetzt und die Änderung im audit_log (Feld-Diff) protokolliert.
  • Gegeben eine cedula_number, die bereits einem anderen Kontakt gehört, wenn ich sie speichere, dann wird die Aktion abgelehnt (harter cedula_number-UNIQUE) — Cédula ist der Golden-Record-Key.
  • Gegeben eine cedula_number im Falschformat, wenn ich speichere, dann wird sie abgelehnt (chk_cedula_format ^\d{3}-\d{7}-\d{1}$).
  • Gegeben ein als merged markierter (gemergter) Kontakt, wenn ich ihn öffne, dann ist er read-only und erscheint nicht in Selektoren/neuen FK-Referenzen.

Traceability: contacts (§2.1), Lösch-/Status-Regeln CRM §7; audit_log (§6.1).


US-CRM-05 — Verlauf-Tab im Kontaktdetail

Als READONLY (Treuhänder) möchte ich je Kontakt eine chronologische Änderungs-Historie sehen, damit ich Mutationen revisionssicher nachvollziehen kann.

Akzeptanzkriterien

  • Gegeben ein Kontakt, wenn ich den Tab „Verlauf" öffne, dann zeigt er audit_log gefiltert auf table_name='contacts', row_pk=id als Timeline (Akteur · Zeit · Feld-Diff), neueste zuerst.
  • Gegeben der Verlauf, wenn ich versuche einen Eintrag zu ändern/löschen, dann ist das für jede Rolle unmöglich (audit_log append-only, nie U/D).

Traceability: audit_log (§6.1), CRM IST-Nachtrag BR-13/14; RLS §13 (audit_log).


US-CRM-06 — Mehrfach-Rollen verwalten

Als OPERATIONS möchte ich einem Kontakt mehrere Rollen geben/entziehen, damit ein Mensch gleichzeitig z. B. Kunde und Affiliate sein kann.

Akzeptanzkriterien

  • Gegeben ein Kontakt, wenn ich eine Rolle (customer/lead/supplier/affiliate/recipient_dr) hinzufüge, dann entsteht eine contact_roles-Zeile (active=true); jede Rolle pro Kontakt nur einmal (UNIQUE (contact_id, role)).
  • Gegeben eine bestehende Rolle, wenn ich sie „entferne", dann wird sie auf active=false gesetzt (deaktiviert, nicht gelöscht — historisch erhalten).
  • Gegeben ein Kontakt mit Rolle supplier, wenn ich recipient_dr hinzufügen will, dann wird es abgelehnt (Konsistenzregel: CH-Partei ≠ DR-Empfänger).
  • Gegeben Rolle affiliate wird gesetzt, dann wird das Folge-Event contact.role_affiliate_added ausgelöst (Modul 7 legt affiliates-Satz an).
  • Gegeben BUCHHALTUNG/READONLY/FAHRER, wenn sie Rollen ändern wollen, dann ist das verwehrt (RLS: nur ADMIN/OPERATIONS schreiben auf contact_roles).

Traceability: contact_roles (§2.2, K-01), RLS §13; Events CRM §6.


US-CRM-07 — Empfänger DR anlegen & verknüpfen

Als OPERATIONS möchte ich zu einem Absender-Kontakt einen Empfänger in der DR anlegen, damit Aufträge/Sendungen eine vollständige Zustelladresse haben.

Akzeptanzkriterien

  • Gegeben ein bestehender Empfänger, wenn ich (Freitext auf full_name/alias/Cédula) suche und ihn wähle, dann wird er direkt mit dem Auftrag verknüpft (keine Doppelanlage).
  • Gegeben ein neuer Empfänger, wenn ich speichere, dann sind Vorname*, Nachname* und province* Pflicht; die Zeile referenziert den Absender (contact_id), Default preferred_lang='es'.
  • Gegeben ein Absender-Kontakt, wenn ich mehrere Empfänger anlege, dann sind beliebig viele recipients pro contact_id möglich (1 Kunde → n Empfänger).
  • Gegeben ein Cédula-Scan via Kamera, wenn OCR Felder vorbefüllt, dann werden sie vor dem Speichern manuell bestätigt (kein vollautomatischer Schritt).

Traceability: recipients (§2.3, K-04), Workflow §4.5; OCR-Verzahnung Modul 2.


US-CRM-08 — Empfänger-Personendaten maskiert (RLS-Negativfall)

Als READONLY (Treuhänder) möchte ich Empfänger nur maskiert sehen, damit schützenswerte DR-Personendaten (revDSG) nicht offenliegen.

Akzeptanzkriterien

  • Gegeben ich bin READONLY, wenn ich Empfänger lese, dann sehe ich sie maskiert (kein cedula_number, kein cedula_scan_path) und kann nichts schreiben.
  • Gegeben ich bin FAHRER, wenn ich die Zustellliste öffne, dann sehe ich nur Lieferfelder (full_name, phone_primary, province, municipality, neighborhood, street, address_reference) — keine KYC-Daten.
  • Gegeben ich bin AFFILIATE, wenn ich auf recipients zugreife, dann wird der Zugriff komplett verweigert (kein Recht in der Matrix).

Traceability: RLS §13 (recipients: RO R* maskiert, FAH Lieferfelder, AFF ); revDSG CRM §8.


US-CRM-09 — Automatische Duplikat-Erkennung (Fuzzy-Match)

Als OPERATIONS möchte ich, dass das System Dubletten-Kandidaten automatisch erkennt, damit ich sie gezielt bereinigen kann statt manuell zu suchen.

Akzeptanzkriterien

  • Gegeben der Hintergrundprozess läuft, wenn ein Paar einen Gesamt-Score ≥ 0.75 erreicht (Telefon exakt 1.0 · E-Mail exakt 0.9 · Name-Trigram ≥ 0.7 → 0.6), dann entsteht eine contact_duplicates-Zeile mit score, match_fields[], resolution='unresolved'.
  • Gegeben ein bereits erfasstes Paar, wenn der Prozess erneut läuft, dann entsteht kein zweiter Eintrag (UNIQUE (contact_a_id, contact_b_id) + kanonische Ordnung contact_a_id < contact_b_id verhindert Spiegelpaare).
  • Gegeben ein Kontakt mit bereits existierender E-Mail/Telefonnummer, wenn ich speichern will, dann erscheint eine Dubletten-Warnung + Merge-Vorschlag (Eindeutigkeit angestrebt, kein stilles Duplikat; #20). cedula_number bleibt harter Unique-Key.

Traceability: contact_duplicates (§2.4), Workflow §4.6; pg_trgm-Indizes (§2.1). (Audit-Verzahnung: E-Mail-Dedup statt harter Eindeutigkeit, BR-P03.)


US-CRM-10 — Duplikate auflösen: Merge zum Golden Record

Als OPERATIONS/ADMIN möchte ich ein Duplikat-Paar zu einem Golden Record zusammenführen, damit die Stammdaten konsolidiert und revisionssicher bleiben.

Akzeptanzkriterien

  • Gegeben ein offenes Paar in der Review-Queue (nach Score sortiert), wenn ich den Golden Record wähle und je Feld den überlebenden Wert bestätige, dann führt eine service_role-Funktion atomar aus: Golden Record aktualisiert; Source status='merged', golden_record_id=target.id, is_golden_record=false.
  • Gegeben der Merge läuft, dann werden alle FKs (orders, quotes, invoices, movements) und alle recipients des Source auf target.id umgebogen, ein unveränderlicher contact_merge_log-Eintrag mit JSON-Snapshots geschrieben und contact_duplicates.resolution='merged' gesetzt.
  • Gegeben der Source hat offene Aufträge/Rechnungen, wenn ich merge, dann erscheint eine Warnung, der Merge ist aber möglich (alle FKs migrieren).
  • Gegeben der Source besitzt einen app_users-Login (E14), wenn ich merge, dann gibt die Funktion eine Warnung zurück und führt keinen automatischen Auth-Schritt aus (manuelle Deaktivierung durch ADMIN — 🔲 offen).
  • Gegeben irgendeine Rolle, wenn sie contact_merge_log ändern/löschen will, dann ist das gesperrt (nur via service_role beschreibbar, OR 957).

Traceability: contact_merge_log (§2.4), Workflow §4.6, Merge-Edge-Cases CRM §7/§9; RLS §13 (contact_merge_log R-only, Schreiben svc).


US-CRM-11 — Paar als „kein Duplikat" markieren

Als OPERATIONS möchte ich ein fälschlich erkanntes Paar als Nicht-Duplikat markieren, damit echte Personen nicht versehentlich verschmelzen und die Queue sauber bleibt.

Akzeptanzkriterien

  • Gegeben ein offenes Paar, wenn ich „Kein Duplikat" wähle, dann wird resolution='not_duplicate' gesetzt (mit resolved_by/resolved_at) und das Paar verschwindet aus der Queue.
  • Gegeben ein als not_duplicate markiertes Paar, wenn der Erkennungsprozess erneut läuft, dann taucht es nicht wieder als offen auf (kein erneutes Aufpoppen desselben Scores).

Traceability: contact_duplicates.resolution (§2.4), Workflow §4.6.


US-CRM-12 — Empfänger → Kunde befördern (BR-P15)

Als OPERATIONS möchte ich einen DR-Empfänger zu einem eigenständigen Kunden/Absender befördern, damit ein bisheriger Empfänger selbst Aufträge erteilen kann — ohne die Logistik-Historie zu verlieren.

Akzeptanzkriterien

  • Gegeben eine recipients-Zeile, wenn ich „Als Kunde anlegen" auslöse, dann sucht das System per Fuzzy-Match (Cédula exakt; sonst Name+Telefon) einen bestehenden contacts-Golden-Record und verknüpft ihn; bei keinem Treffer wird aus den Empfängerdaten (first_name, last_name, phone_primary, cedula_number, email, alias) ein neuer contacts-Satz erzeugt.
  • Gegeben der neue/verknüpfte Kontakt, dann wird contact_roles role='customer' gesetzt und status='active'.
  • Gegeben die Beförderung, dann bleibt die recipients-Zeile bestehen (Zustelldaten/Historie) und wird über recipients.promoted_contact_id mit dem Kunden verknüpft; ein audit_log-Eintrag recipient.promoted_to_customer wird geschrieben.
  • Gegeben die 2-Ebenen-Hierarchie (Absender = contacts, Empfänger = recipients), dann bleibt sie erhalten (kein Aufheben der Trennung Logistik ↔ Geschäfts-Partei).

Traceability: recipients.promoted_contact_id (§2.3), CRM-Abschnitt „Empfänger → Kunde befördern" (BR-P15); audit_log (§6.1).


US-CRM-13 — Suche & Autocomplete für Selektoren

Als OPERATIONS möchte ich Kontakte/Empfänger schnell per Tippsuche finden, damit ich sie in Offerten/Sendungen ohne Scrollen auswählen kann.

Akzeptanzkriterien

  • Gegeben ein Kontakt-Selector (Offerten/Aufträge), wenn ich tippe, dann durchsucht die Autocomplete display_name, alias, phone_primary, email (Fuzzy via pg_trgm) und liefert begrenzte Trefferliste — nur status IN ('active','lead') und passende Rollen.
  • Gegeben ein Empfänger-Selector (Sendungen), wenn ich tippe, dann matcht die Suche full_name, alias und cedula_number.
  • Gegeben ein gemergter Kontakt (status='merged'), wenn ich suche, dann erscheint er nicht in den Vorschlägen (is_golden_record=true-Filter).

Traceability: idx_contacts_trgm_name/idx_contacts_trgm_alias (§2.1), recipients (§2.3); Offerten-Selector-Regel CRM §6.


US-CRM-14 — Maskierte Sicht für Treuhänder (Field-Level-RLS)

Als READONLY (Treuhänder/Gast) möchte ich Kontakte lesen, aber keine Ausweisdaten sehen, damit ich prüfen kann, ohne besonders schützenswerte Daten zu erhalten.

Akzeptanzkriterien

  • Gegeben ich bin READONLY, wenn ich einen Kontakt öffne, dann sehe ich Stammdaten, aber nicht cedula_number/id_scan_storage_path; jeder Schreibversuch wird verweigert.
  • Gegeben ich bin FAHRER, wenn ich Kontakte lese, dann sind nur id, display_name, phone_primary, status sichtbar — sonst nichts.
  • Gegeben ich bin AFFILIATE, wenn ich Kontakte abrufe, dann sehe ich keine fremden Kunden (nur eigenen gemappten Satz, falls vorhanden).
  • Gegeben ein Ausweis-Scan im privaten Storage-Bucket, wenn ein Berechtigter ihn öffnet, dann nur via signierte URL mit kurzer TTL (kein Public Access).

Traceability: RLS §13 (contacts: RO R* kein cedula/scan, FAH Feld-Subset, AFF R(own)); revDSG/Storage CRM §8.


US-CRM-15 — Lösch-Schutz & Anonymisierung (revDSG ↔ OR 957)

Als ADMIN möchte ich ein berechtigtes Löschbegehren erfüllen, ohne die Buchführungspflicht zu verletzen, damit revDSG und OR 957 gleichzeitig eingehalten werden.

Akzeptanzkriterien

  • Gegeben ein Kontakt mit verknüpften orders/invoices/movements, wenn ich ihn hart löschen will, dann wird das abgelehnt (ON DELETE RESTRICT) — möglich ist nur status='inactive' oder Merge.
  • Gegeben ein Auskunfts-/Löschbegehren (Art. 32 revDSG) ohne Retentionspflicht, wenn ich es bearbeite, dann ist ein Daten-Export und anschliessend physische Löschung möglich.
  • Gegeben ein buchungsrelevanter Kontakt (in movements referenziert), wenn ein Löschbegehren eingeht, dann greift die 10-jährige Aufbewahrungspflicht — statt Löschung erfolgt Anonymisierung/Sperrung, dokumentiert im audit_log.

Traceability: Lösch-Schutz CRM §7, revDSG/OR-957 CRM §8; audit_log (§6.1), Periodensperre/Invarianten §13.


Schätzung & Priorität (Vorschlag — vom Team final zu bestätigen)

StoryGrößePriorität (MoSCoW)
US-CRM-01 Lead-Intake WebsiteMMust
US-CRM-02 Lead aus WhatsAppSShould
US-CRM-03 Kontakt manuell anlegenMMust
US-CRM-04 Kontakt bearbeitenMMust
US-CRM-05 Verlauf-TabSShould
US-CRM-06 Mehrfach-RollenMMust
US-CRM-07 Empfänger anlegen/verknüpfenMMust
US-CRM-08 Empfänger maskiert (RLS)SMust
US-CRM-09 Duplikat-Erkennung (Fuzzy)LShould
US-CRM-10 Merge → Golden RecordLMust
US-CRM-11 „Kein Duplikat" markierenSShould
US-CRM-12 Empfänger→Kunde (BR-P15)MShould
US-CRM-13 Suche & AutocompleteMMust
US-CRM-14 Maskierte Treuhänder-SichtMMust
US-CRM-15 Lösch-Schutz & AnonymisierungMShould

Größe = grobe Aufwands-Indikation (S/M/L), nicht Story Points. Priorität nach MoSCoW (Must/Should/Could). Beides ist ein Vorschlag zur Release-Planung — die verbindliche Schätzung/Priorisierung macht das Team im Backlog.


Abdeckung & offene Punkte (dieses Epic)

  • 15 Stories decken den Kontakt-Lebenszyklus end-to-end ab (Lead-Intake Website/WhatsApp → manuell anlegen → bearbeiten/Verlauf → Mehrfach-Rollen → Empfänger anlegen/maskiert → Dedup-Erkennung → Merge/kein-Duplikat → Empfänger→Kunde-Promotion → Suche/Autocomplete → Treuhänder-Maskierung → Lösch-Schutz/Anonymisierung) plus Querschnitt (RLS, Audit, revDSG/OR 957).
  • Audit-Verzahnung: US-CRM-01/04/05 (audit_log-Mutationsprotokoll + append-only), US-CRM-09 (E-Mail-Dedup statt harter Eindeutigkeit, BR-P03), US-CRM-10 (contact_merge_log unveränderlich, OR 957), US-CRM-15 (Anonymisierung statt Löschung bei Retentionspflicht) — die Akzeptanzkriterien encodieren die Compliance-Härtungen als testbare Szenarien.
  • RLS-Negativfälle: US-CRM-06 (Rollen-Schreiben nur ADMIN/OPERATIONS), US-CRM-08 + US-CRM-14 (READONLY maskiert, FAHRER Feld-Subset, AFFILIATE kein Fremdzugriff) — direkt aus der Matrix §13 abgeleitet.
  • Entscheid-abhängig:
    • E9/#20 (E-Mail-/Telefon-Eindeutigkeit, entschieden 2026-06-28): Eindeutigkeit angestrebt mit Dubletten-Warnung + Merge-Vorschlag (US-CRM-09); kein stilles Duplikat; Cédula = harter Unique-Key.
    • E14 (app_users ↔ contacts): Trennung Login-Identität (app_users) ↔ Geschäfts-Partei (contacts) ist gesetzt (E-05/E-06); offen bleibt das Auth-User-Verhalten beim Merge eines Kontakts mit Login (US-CRM-10: Warnung, kein Auto-Schritt) 🔲 (§14 Auth-User-Merge).
    • Telefon-Uniqueness: Soft-Warning vs. harter UNIQUE auf phone_primary (betrifft US-CRM-03/09) 🔲.
    • Duplikat-Score-Algorithmus: pg_trgm vs. dedizierte Edge-Function-Gewichtung (betrifft US-CRM-09) 🔲.
  • Nächster Schritt: Format-Freigabe → Fan-out auf die übrigen Epics (WhatsApp/OCR, Produkte/Preise, Offerten, Finanzen, Affiliate, Plattform) im selben Muster + INVEST-Red-Team-Pass.