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/AFFILIATEnur als Lese-Negativfall). IDs:US-CRM-NN(stabil). Status: Entwurf (zur Freigabe).
Personas in diesem Epic
| Rolle | Person | Bezug zum CRM |
|---|---|---|
ADMIN | Marcel/Mariela | alles — anlegen, bearbeiten, mergen, löschen/anonymisieren |
BUCHHALTUNG | Mariela | Kontakte lesen + anlegen/bearbeiten (kein Merge-Delete, kein Rollen-Schreiben) |
OPERATIONS | Markus | Kontakte/Empfänger/Rollen anlegen + bearbeiten, Dedup auflösen (kein Delete) |
READONLY | Treuhänder/Gast | nur 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_honeyleer), wenn kein Treffer mit Score ≥ 0.85 existiert, dann wird eincontacts-Datensatz mitstatus='lead',lead_source='website_form'und einecontact_roles-Zeilerole='lead'angelegt + einaudit_log-Eintragcontact.createdgeschrieben. - 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 — Auditcontact.updated_from_lead. - Gegeben
_honeyist nicht leer (Bot), wenn der Request eingeht, dann passiert keine Datenänderung und die Antwort ist200ohne Aktion. - Gegeben
phonefehlt/ungültig, wenn der Lead anlegt wird, dann bleibtphone_primary=NULLundneeds_verification=true(Lead trotzdem erstellt);emailist optional. - Gegeben
type='Programa Afiliación', wenn der Lead anlegt wird, dann wird zusätzlichcontact_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 einaudit_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_langDefaultes;lead_source='manual'. - Gegeben ich wähle die Rolle
customeraus, wenn ich speichere, dann wirdstatus='active'gesetzt (stattlead). - 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ürphone_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_bygesetzt und die Änderung imaudit_log(Feld-Diff) protokolliert. - Gegeben eine
cedula_number, die bereits einem anderen Kontakt gehört, wenn ich sie speichere, dann wird die Aktion abgelehnt (hartercedula_number-UNIQUE) — Cédula ist der Golden-Record-Key. - Gegeben eine
cedula_numberim Falschformat, wenn ich speichere, dann wird sie abgelehnt (chk_cedula_format^\d{3}-\d{7}-\d{1}$). - Gegeben ein als
mergedmarkierter (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_loggefiltert auftable_name='contacts',row_pk=idals 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_logappend-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 einecontact_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=falsegesetzt (deaktiviert, nicht gelöscht — historisch erhalten). - Gegeben ein Kontakt mit Rolle
supplier, wenn ichrecipient_drhinzufügen will, dann wird es abgelehnt (Konsistenzregel: CH-Partei ≠ DR-Empfänger). - Gegeben Rolle
affiliatewird gesetzt, dann wird das Folge-Eventcontact.role_affiliate_addedausgelöst (Modul 7 legtaffiliates-Satz an). - Gegeben
BUCHHALTUNG/READONLY/FAHRER, wenn sie Rollen ändern wollen, dann ist das verwehrt (RLS: nur ADMIN/OPERATIONS schreiben aufcontact_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), Defaultpreferred_lang='es'. - Gegeben ein Absender-Kontakt, wenn ich mehrere Empfänger anlege, dann sind beliebig viele
recipientsprocontact_idmö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 (keincedula_number, keincedula_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 aufrecipientszugreife, 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 mitscore,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 Ordnungcontact_a_id < contact_b_idverhindert 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_numberbleibt 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; Sourcestatus='merged',golden_record_id=target.id,is_golden_record=false. - Gegeben der Merge läuft, dann werden alle FKs (
orders,quotes,invoices,movements) und allerecipientsdes Source auftarget.idumgebogen, ein unveränderlichercontact_merge_log-Eintrag mit JSON-Snapshots geschrieben undcontact_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 viaservice_rolebeschreibbar, 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 (mitresolved_by/resolved_at) und das Paar verschwindet aus der Queue. - Gegeben ein als
not_duplicatemarkiertes 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 bestehendencontacts-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 neuercontacts-Satz erzeugt. - Gegeben der neue/verknüpfte Kontakt, dann wird
contact_roles role='customer'gesetzt undstatus='active'. - Gegeben die Beförderung, dann bleibt die
recipients-Zeile bestehen (Zustelldaten/Historie) und wird überrecipients.promoted_contact_idmit dem Kunden verknüpft; einaudit_log-Eintragrecipient.promoted_to_customerwird 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 — nurstatus IN ('active','lead')und passende Rollen. - Gegeben ein Empfänger-Selector (Sendungen), wenn ich tippe, dann matcht die Suche
full_name,aliasundcedula_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 nichtcedula_number/id_scan_storage_path; jeder Schreibversuch wird verweigert. - Gegeben ich bin
FAHRER, wenn ich Kontakte lese, dann sind nurid,display_name,phone_primary,statussichtbar — 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 nurstatus='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
movementsreferenziert), wenn ein Löschbegehren eingeht, dann greift die 10-jährige Aufbewahrungspflicht — statt Löschung erfolgt Anonymisierung/Sperrung, dokumentiert imaudit_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)
| Story | Größe | Priorität (MoSCoW) |
|---|---|---|
| US-CRM-01 Lead-Intake Website | M | Must |
| US-CRM-02 Lead aus WhatsApp | S | Should |
| US-CRM-03 Kontakt manuell anlegen | M | Must |
| US-CRM-04 Kontakt bearbeiten | M | Must |
| US-CRM-05 Verlauf-Tab | S | Should |
| US-CRM-06 Mehrfach-Rollen | M | Must |
| US-CRM-07 Empfänger anlegen/verknüpfen | M | Must |
| US-CRM-08 Empfänger maskiert (RLS) | S | Must |
| US-CRM-09 Duplikat-Erkennung (Fuzzy) | L | Should |
| US-CRM-10 Merge → Golden Record | L | Must |
| US-CRM-11 „Kein Duplikat" markieren | S | Should |
| US-CRM-12 Empfänger→Kunde (BR-P15) | M | Should |
| US-CRM-13 Suche & Autocomplete | M | Must |
| US-CRM-14 Maskierte Treuhänder-Sicht | M | Must |
| US-CRM-15 Lösch-Schutz & Anonymisierung | M | Should |
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_logunverä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_trgmvs. 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.