User Stories — Epic 7: Affiliate-Programm

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 7 (16-affiliate.md), Schema Dok 30 §8 (affiliates, referral_codes, commission_entries, commission_payouts, affiliate_users), RLS Dok 30 §13. Rollen-Codes kanonisch (UPPERCASE): ADMIN, BUCHHALTUNG, OPERATIONS, AFFILIATE, READONLY. IDs: US-AFF-NN (stabil). Status: Entwurf (zur Freigabe).


Personas in diesem Epic

RollePersonBezug zum Affiliate-Programm
ADMINMarcelAffiliates + Codes verwalten, Provisions-Freigabe, Payout freigeben/auszahlen, Storno/Korrektur
BUCHHALTUNGMarielaProvisions-Prüfung/-Freigabe, Payout-Lauf, Auszahlung ins movements-Ledger
OPERATIONSMarkusCodes anwenden (Offerte/Auftrag), Status/Notiz an Provisionen — kein Payout
AFFILIATEextern (Mini-Portal)sieht nur eigene Codes, Provisionen, vermittelte Sendungen — R(own) via current_affiliate_id()
READONLYTreuhänder/Gastnur lesen (Provisions-Aggregate granular 🔲)

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-AFF-01 — Affiliate anlegen (aus Kontakt)

Als ADMIN möchte ich einen bestehenden Kontakt als Affiliate erfassen (Vertragsdaten + Provisions-Default + Payout-Methode), damit Vermittler strukturiert statt in Excel geführt werden.

Akzeptanzkriterien

  • Gegeben ein Kontakt ohne Affiliate-Satz, wenn ich „Affiliate anlegen" mit contract_start, default_commission_type/value und Payout-Methode speichere, dann entsteht ein affiliates-Datensatz mit status='active' und ein Audit-Log-Eintrag.
  • Gegeben ein Kontakt, der bereits Affiliate ist, wenn ich ihn erneut anlege, dann wird die Aktion abgelehnt („Kontakt ist bereits als Affiliate erfasst", UNIQUE affiliates_contact_id_unique).
  • Gegeben das Provisions-Modell (#18, entschieden 2026-06-28): default_commission_type='percentage', default_commission_value = individueller %-Satz je Affiliate (Pflichtfeld beim Anlegen). Der konkrete %-Satz wird pro Affiliate gesetzt; 🔲 offen bleiben nur Staffeln (mehr % ab X Sendungen — §9.1).

Traceability: affiliates (§8), affiliates_contact_id_unique, Workflow §4.1.


US-AFF-02 — Referral-Code(s) erstellen

Als ADMIN möchte ich einem Affiliate einen oder mehrere eindeutige Referral-Codes mit Kundenrabatt- und Provisions-Regel (%-Satz, #18) anlegen, damit er sie an Kunden weitergeben kann.

Akzeptanzkriterien

  • Gegeben ein Affiliate, wenn ich einen Code mit Rabatt-Regel (Typ/Wert, optionale CHF-Obergrenze) und Gültigkeit speichere, dann entsteht ein referral_codes-Satz mit is_active=true und uses_count=0.
  • Gegeben ein bereits vergebener Code-String, wenn ich speichere, dann wird er abgelehnt (UNIQUE referral_codes_code_unique) mit Alternativvorschlag.
  • Gegeben leere Provisions-Felder, dann gilt der Affiliate-Default (commission_type/value = NULL ⇒ Fallback auf affiliates.default_*); gegeben valid_until < valid_from, dann Validierungsfehler.

Traceability: referral_codes (§8), referral_codes_code_unique, §4.2.


US-AFF-03 — Code in Offerte anwenden (Rabatt-Snapshot)

Als OPERATIONS möchte ich einen Referral-Code in einer Offerte eintragen und den Rabatt live sehen, damit der Kunde die Vergünstigung erhält und die Vermittlung verankert ist.

Akzeptanzkriterien

  • Gegeben ein gültiger Code (is_active, heute in [valid_from, valid_until], uses_count < max_uses), wenn ich ihn in der Offerte setze, dann werden Rabatt (discount_from_code_chf) und ein Snapshot der Regel (discount_from_code_type/value) auf der Offerte gespeichert und der Affiliate readonly angezeigt.
  • Gegeben ein abgelaufener Code oder erschöpftes Limit, wenn ich ihn setze, dann wird er abgelehnt (Fehlermeldung mit Ablaufdatum bzw. „maximale Nutzungen erreicht").
  • Gegeben ein Code eines paused/terminated-Affiliates, wenn ich ihn setze, dann erscheint eine gelbe Warnung, aber kein Hard-Block; gegeben der Code wird wieder entfernt, dann wird discount_from_code_chf = NULL und kein commission_entries-Satz entsteht.

Traceability: quotes.referral_code_id/discount_from_code_* (§8), §4.3.


US-AFF-04 — Code Offerte → Auftrag übertragen

Als OPERATIONS möchte ich, dass Code + Affiliate beim Annehmen der Offerte automatisch in den Auftrag übergehen und die Provisionsanwartschaft (pending) angelegt wird, damit die Vermittlung lückenlos bis zur Abrechnung mitläuft.

Akzeptanzkriterien

  • Gegeben eine akzeptierte Offerte mit Code, wenn der Auftrag entsteht (Server Action, quotes ACCEPTED→CONVERTED), dann werden orders.referral_code_id und orders.affiliate_id 1:1 übernommen und referral_codes.uses_count per Trigger um 1 erhöht.
  • Gegeben dieser Übergang, dann wird der commission_entries-Satz mit status='pending' angelegt (Entstehung bei Konversion, #19 — US-AFF-05). Die Freigabe (approved) folgt erst beim Tracking-DELIVERED (US-AFF-06), nicht bei invoices → PAID.
  • Gegeben ein zwischenzeitlich deaktivierter Code, wenn der Auftrag entsteht, dann Warnung an die Sachbearbeitung, Auftrag dennoch möglich (Snapshot in Offerte ist maßgeblich).

Traceability: orders.referral_code_id/affiliate_id (§8), §4.4; Server-Action-Übergang Dok 30 §13 (Fussnote 4).


US-AFF-05 — Provisions-Entstehung bei Offerte-Konversion (Lifecycle-Kern, #19/F-13)

Als BUCHHALTUNG möchte ich, dass eine Provision (pending) entsteht, sobald eine Offerte mit Affiliate-Code zum Auftrag konvertiert wird, damit die vermittelte Sendung von Anfang an eine nachvollziehbare Provisionsanwartschaft trägt — die Fälligkeit folgt erst mit der Zustellung (US-AFF-06).

Akzeptanzkriterien

  • Gegeben eine akzeptierte Offerte mit referral_code_id IS NOT NULL, wenn sie zum Auftrag konvertiert wird (quotes ACCEPTED → CONVERTED), dann wird automatisch ein commission_entries-Satz mit status='pending' angelegt: base_amount_chf = Netto-Sendungswert, Regel aus referral_codes (sonst Affiliate-Default; #18 %-Satz je Affiliate), commission_amount_chf berechnet (% bzw. Fix).
  • Gegeben der Rechnungsstatus der Sendung (OPEN/PARTIAL/PAID), dann hat er keinen Einfluss auf Entstehung oder Fälligkeit der Provision (Geschäftsentscheid 2026-06-28 #19: invoices.status='PAID' ist nicht der Auslöser; die frühere Bindung ist aufgehoben).
  • Gegeben commission_value = 0, dann wird der Eintrag trotzdem angelegt (Nachweis) mit UI-Hinweis; gegeben ein Auftrag ohne affiliate_id, dann kein Eintrag.

Traceability: commission_entries (§8), Lifecycle #19/F-13 (Dok 30 §8); Events §6, Modul 16 §4.5 A. (Verzahnt Audit content-02 — Entscheid 2026-06-28: Entstehung bei Konversion, Fälligkeit bei DELIVERED, nicht an PAID.)


US-AFF-06 — Provisions-Freigabe (pending → approved) automatisch bei DELIVERED (#19)

Als BUCHHALTUNG möchte ich, dass eine ausstehende Provision automatisch freigegeben wird, sobald die vermittelte Sendung zugestellt ist, damit nur tatsächlich erbrachte Vermittlungen in einen Payout fließen — ohne manuellen Prüfschritt im Normalfall.

Akzeptanzkriterien

  • Gegeben ein commission_entries-Satz im Status pending, wenn die zugehörige Sendung den Tracking-Status DELIVERED erreicht, dann setzt ein DB-Trigger (auf tracking_events/Sendungsabschluss, Modul 5) den Satz auf approved mit approved_at/approved_by=system und Audit-Log-Eintrag (Geschäftsentscheid 2026-06-28 #19). Verknüpfung über shipments.order_id → commission_entries.order_id.
  • Gegeben die Sendung ist noch nicht DELIVERED, dann bleibt der Satz pending und ist nicht abrechnungsfähig; eine manuelle Freigabe durch BUCHHALTUNG/ADMIN bleibt als Override möglich (Einzel-/Bulk-Freigabe mit Audit-Log).
  • Gegeben die Rechnung/Sendung wird nachträglich storniert, wenn der Eintrag noch pending/approved ist, dann wechselt er auf cancelled (Trigger bei Storno).

Traceability: commission_entries.status/approved_* (§8), §4.5 B/§4.6, Lifecycle #19/F-13. (Audit content-02 — Entscheid 2026-06-28: DELIVERED = automatischer Freigabe-Auslöser per DB-Trigger.)


US-AFF-07 — Keine Doppelprovision je Auftrag (Daten-Invariante)

Als ADMIN möchte ich, dass pro Auftrag höchstens eine Provision existiert, damit Mehrfachzahlungen für dieselbe Sendung systemisch ausgeschlossen sind.

Akzeptanzkriterien

  • Gegeben ein Auftrag mit bereits bestehendem commission_entries-Satz, wenn die Entstehung erneut ausgelöst würde (z.B. Re-Run der Konversion, doppelter DELIVERED-Freigabe-Lauf), dann entsteht kein zweiter Eintrag — commission_entries_order_uniq UNIQUE(order_id) verhindert das Duplikat, die App zeigt eine Warnung statt eines Fehlers.
  • Gegeben commission_entries.order_id, dann ist es NOT NULL (jede Provision ist genau einer vermittelten Sendung zugeordnet — keine „freischwebende" Provision).

Traceability: commission_entries_order_uniq, order_id NOT NULL (§8); §7 (Doppelprovision). (Deckt Audit data-03 direkt ab.)


US-AFF-08 — Payout-Lauf erstellen & freigeben

Als BUCHHALTUNG möchte ich freigegebene Provisionen eines Affiliates für einen Zeitraum zu einem Payout bündeln, damit die Auszahlung gesammelt und nachvollziehbar erfolgt.

Akzeptanzkriterien

  • Gegeben ein Affiliate mit approved-Einträgen im Zeitraum, wenn ich einen Payout erstelle, dann entsteht ein commission_payouts-Satz mit status='draft', die Einträge werden via payout_id verknüpft und total_amount_chf = Summe der Einträge.
  • Gegeben keine freigegebenen Einträge im Zeitraum, wenn ich erstelle, dann „Keine abrechnungsfähigen Provisionen" — kein Payout angelegt.
  • Gegeben ein draft-Payout, wenn ich ihn prüfe und bestätige, dann status='approved'; gegeben ein fehlendes Payout-Konto des Affiliates, dann Warnung bei Erstellung, aber kein Block des Entwurfs.

Traceability: commission_payouts (§8), §4.6 (Schritte 1–6).


US-AFF-09 — Auszahlung ins Ledger (paidmovements)

Als BUCHHALTUNG möchte ich den freigegebenen Payout auszahlen und revisionssicher als Kreditor buchen, damit Provisionen korrekt im Finanz-Ledger erscheinen.

Akzeptanzkriterien

  • Gegeben ein Payout in approved, wenn ich auszahle, dann wechselt er auf paid und es entsteht genau ein movements-Eintrag (movement_type='SUPPLIER_PAYMENT', source_type='commission_payout', source_id=payout.id, contact_id=affiliate.contact_id); commission_payouts.movement_id und alle zugehörigen commission_entries.movement_id + status='paid' werden gesetzt (AP-9).
  • Gegeben der Auto-Posting-Lauf wiederholt sich, dann entsteht kein zweiter movements-Eintrag (Unique-Constraint auf source_type/source_id).
  • Gegeben ein paid-Payout, wenn jemand ihn zurücksetzen will, dann wird das abgelehnt (nur approved → draft erlaubt; paid final, Buchung unveränderlich — API 409).

Traceability: commission_payouts.status/movement_id, movements (§8), Auto-Posting AP-9 (Dok 30 §11.1); §7 (Payout-Rücksetzen). (Revisionssicherheit OR 957 / GeBüV.)


US-AFF-10 — Affiliate-Mini-Portal: nur eigene Daten (RLS-Negativfall)

Als AFFILIATE möchte ich in meinem Portal ausschließlich meine eigenen Codes, Provisionen, Payouts und vermittelten Sendungen sehen, damit Mandantentrennung gewahrt ist und ich keine fremden Daten sehe.

Akzeptanzkriterien

  • Gegeben ich bin als AFFILIATE eingeloggt (Brücke affiliate_userscurrent_affiliate_id()), wenn ich Codes/Provisionen/Payouts öffne, dann sehe ich nur Zeilen mit meinem affiliate_id (R(own)), und vermittelte Sendungen nur über Offerten/Aufträge mit referral_code_id/affiliate_id = current_affiliate_id().
  • Gegeben ein fremder commission_entries-/referral_codes-Satz, wenn ich ihn per ID/URL abrufe, dann liefert die RLS kein Ergebnis (kein Fremdeinblick, auch nicht in Beträge fremder Offerten — §13 Fussnote 5).
  • Gegeben ich versuche zu schreiben (Code anlegen, Provision freigeben, Payout buchen), dann wird es abgelehnt — AFFILIATE hat ausschließlich SELECT auf eigene Zeilen.

Traceability: affiliate_users, current_affiliate_id(); RLS-Matrix §13 (R(own) für affiliates/referral_codes/commission_entries/commission_payouts), Fussnoten 5 + 11. (Deckt AFFILIATE-Mandantentrennung-Audit direkt ab.)


US-AFF-11 — IBAN/Kontodaten verschlüsselt & DSG-Löschung

Als ADMIN möchte ich, dass Affiliate-Kontodaten (IBAN) schützenswert gespeichert und auf Anfrage anonymisierbar sind, damit DSG/revDSG und Revisionspflicht zugleich erfüllt sind.

Akzeptanzkriterien

  • Gegeben ich erfasse payout_iban, dann wird es verschlüsselt/maskiert gespeichert (Supabase Vault bzw. Column-Level-Encryption 🔲 — abhängig vom Tier, §9.3) und nur berechtigten Rollen (ADMIN/BUCHHALTUNG) im Klartext gezeigt.
  • Gegeben eine DSG-Löschanfrage eines Affiliates mit bestehenden Einträgen, wenn ich sie ausführe, dann greift kein Hard-Delete (ON DELETE RESTRICT), sondern ein Anonymisierungs-Workflow (Name → [GELÖSCHT], IBAN → NULL) bei status='terminated'; Provisions-/Payout-Historie bleibt revisionssicher erhalten.

Traceability: affiliates.payout_iban (§8), ON DELETE RESTRICT; §8 (DSG / OR 957). (Verzahnt Audit sec-05/comp-12 — IBAN-Verschlüsselung; entscheid-abhängig 🔲 Vault.)


US-AFF-12 — Storno & Korrektur einer Provision

Als ADMIN möchte ich eine fehlerhafte oder stornierte Provision revisionssicher zurücknehmen können, damit Korrekturen nachvollziehbar bleiben statt überschrieben zu werden.

Akzeptanzkriterien

  • Gegeben ein pending/approved-Eintrag zu einer stornierten Rechnung, wenn der Storno-Trigger feuert, dann wechselt er auf cancelled (kein Hard-Delete) mit Audit-Log-Eintrag.
  • Gegeben ein bereits ausbezahlter (paid) Eintrag, wenn korrigiert werden muss, dann erfolgt das ausschließlich über eine gegensätzliche Storno-Buchung im movements-Ledger — der ursprüngliche movements-Eintrag wird nie gelöscht/überschrieben.

Traceability: commission_entries.status='cancelled' (§8), audit_log; §7/§8 (Storno, OR 957 / GeBüV).


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

StoryGrößePriorität (MoSCoW)
US-AFF-01 Affiliate anlegenSMust
US-AFF-02 Referral-Code(s) erstellenMMust
US-AFF-03 Code in Offerte anwendenMMust
US-AFF-04 Code Offerte → AuftragSMust
US-AFF-05 Provisions-Entstehung (Konversion, #19/F-13)LMust
US-AFF-06 Provisions-FreigabeMMust
US-AFF-07 Keine DoppelprovisionSMust
US-AFF-08 Payout-Lauf erstellenMMust
US-AFF-09 Auszahlung ins LedgerLMust
US-AFF-10 Affiliate-Mini-Portal (RLS)LShould
US-AFF-11 IBAN-Verschlüsselung & DSGMShould
US-AFF-12 Storno & KorrekturMShould

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)

  • 12 Stories decken den Affiliate-Fluss end-to-end ab (Affiliate + Code anlegen → Code in Offerte/Auftrag → Provisions-Entstehung bei Konversion (pending) → Freigabe bei DELIVERED (approved) → Payout-Lauf → Auszahlung ins Ledger) plus Querschnitt (Mini-Portal-RLS, IBAN/DSG, Storno).
  • Audit-Verzahnung: US-AFF-05/-06 (Lifecycle #19: Entstehung bei Offerte-Konversion, Fälligkeit/Freigabe per DB-Trigger bei DELIVERED, entkoppelt von invoices.status='PAID' — content-02), US-AFF-07 (UNIQUE(order_id) + order_id NOT NULL ⇒ keine Doppelprovision — data-03), US-AFF-11 (IBAN-Verschlüsselung — sec-05/comp-12), US-AFF-10 (AFFILIATE-Mandantentrennung via current_affiliate_id(), Negativszenario kein Fremdeinblick) — die Akzeptanzkriterien encodieren die Audit-Härtungen als testbare Szenarien.
  • Entschieden 2026-06-28: Provisions-Trigger-Zeitpunkt = DELIVERED (Freigabe), Entstehung bei Konversion (#19 — US-AFF-04/05/06); Provisions-Modell = %-Satz je Affiliate (#18 — US-AFF-01/02); Code-Rabatt = Kundenrabatt (US-AFF-03).
  • Entscheid-abhängig (🔲 verbleibend): Affiliate-Staffeln (mehr % ab X Sendungen, §9.1), Teil-Sendungen je Auftrag (anteilige vs. Gesamt-Freigabe bei DELIVERED), IBAN-Vault (US-AFF-11, §9.3); ferner Payout-Zyklus (§9.5), Mehrfach-Codes je Sendung (§9.6), MWST auf Provision (§9.7), granulare READONLY-Aggregate (§9.9), Lead-Konvertierungs-Berechtigung (§9.10).
  • Nächster Schritt: Freigabe der Stories → Backlog-Übernahme + INVEST-Red-Team-Pass; Klärung der verbleibenden 🔲-Geschäftsentscheide (insb. Staffeln, Teil-Sendungen, IBAN-Vault) vor Implementierungsbeginn.