mivita/dev/2026-07-22/entwicklungsplan-2026-09-07.md
Kevin cb4eaff95d Doku: Plaene, Kundenbriefe, Log-Analyse und Ledger-Plan versioniert, Stand 23.09.
- September-Plan: B2, E1 (Gleichstand, Zahlungsdatum), A07 und die
  Abo-Warnung nachgetragen; Audit-Stand A07 erledigt.
- Bisher nur im Arbeitsbaum: Entwicklungsplan und Kundenbrief vom 07.09.
  samt PDF, Tickets vom 08.09., Log-Analyse 26.08., Ledger-Plan,
  August-Anerkennungen. E-Mail-Adressen von Kunden und Beratern durch
  Platzhalter bzw. Namen ersetzt.

Nicht versioniert (personenbezogene Daten): Kundenliste CSV/XLSX und die
Produktions-Logs in dev/2026-08-26-logs/*.log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:45:10 +00:00

294 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Entwicklungsplan — Kundenfeedback vom 2026-09-07
Basis: `dev/2026-07-22/mivita_Statistiken_07_09_2026.pdf` (Kundin, gesammelte Rückmeldung vom 30.08./07.09.).
Ist-Stand-Analyse: 2026-09-07, gegen den Stand nach dem Livegang der 08/2026-Pakete.
Das Feedback zerfällt in **15 Pakete plus 2 reine Rückfragen**. Vier Punkte sind reine Beschriftungs-/Spaltenänderungen, vier neue oder umgebaute Ansichten, zwei grundsätzliche Entscheidungen und vier Prozess-Automatisierungen außerhalb der Statistik. Zwei Punkte sind bereits umgesetzt und brauchen nur eine Antwort.
> ## Umsetzungsstand 2026-09-07: K1–K9 umgesetzt
>
> Die Quick-Wins und die drei Ansichts-Pakete sind gebaut und getestet. **K10–K15 sind bewusst offen** — sie hängen an den Rückfragen F3–F8.
>
> | # | Status | Kurz |
> |---|---|---|
> | A1 | ✅ beantwortet | Läuft bereits: 6 Lieferungen / 6 Monate Pause / 3 Tage Sperrfrist. Setting `abo-pause-interval-months` sollte noch explizit auf 6 gesetzt werden (heute Code-Default). |
> | A2 | ✅ bestätigt | Prognose-Punkte stimmen. |
> | K1 | ✅ | Drei Spalten in „Top 10 Kundenabos (Umsatz)": Abgerechnet · davon reine Abo-Punkte · einmalig dazu. Neuer `customerAboAddonPoints()`; `points_recurring` wird als **Rest** aus dem Gesamtwert gebildet, damit beide Spalten immer die angezeigte Summe ergeben. |
> | K2 | ✅ | „Teamabos (direkt)" und „Team-Kundenabos (direkt)" aus der Führungskräfte-Tabelle entfernt. Kopf-Summe bleibt (F1, Variante a) und ist jetzt per Tooltip und Fußnote als überschneidungsfreie Zählung erklärt. |
> | K3 | ✅ | Unterzeile „Basis: Paylinepunkte + persönliches Volumen" unter der Payline-Prognose. |
> | K4 | ✅ | Summe der Payline-Abos ergänzt: „149 Teamabos + 391 Kundenabos = 540 Abos gesamt". |
> | K5 | ✅ | Abo-Bestellungen in der Punkteübersicht türkis mit Zusatz „(Abo)", plus Legende über der Tabelle. |
> | K6 | ✅ | Telefonspalte (mit Mobil-Fallback) im Neupartner-Drilldown inkl. CSV; Spalten umbenannt in „Punkte seiner Kunden" und „Gesamt (inkl. Eigenbestellung)" mit erklärenden Tooltips. |
> | K7 | ✅ | Summenzeile bei „Recruiter des Monats" — **dedupliziert**, nicht als Spaltensumme (sonst zählt das Kettenvolumen doppelt). Eigener Payload-Schlüssel `recruiter_totals`, ältere Snapshots bleiben lesbar. |
> | K8 | ✅ | Eigene Kundenabos als sortierbare Liste mit Suche (Kunde · Liefertag · Punkte · nächste Ausführung · Startdatum · Ausführungen · Status). Abos mit offener Erstzahlung bleiben als Karte darüber, damit deren Aktionsbuttons erhalten bleiben. |
> | K9 | ✅ | Liefertag-Vorschau 5./10./20./25. nur aus den eigenen Kundenabos, über der neuen Liste. |
>
> **Nennenswerte Umsetzungsentscheidungen**
>
> - **K1 an Produktivdaten geprüft:** Für 200 von 200 Abo-Bestellungen aus 08/2026 stimmt `SUM(shopping_order_items.points * qty)` exakt mit `user_sales_volumes.points` überein; die Positionspunkte sind Stückpunkte zum Bestellzeitpunkt. Es gibt produktiv 726 Positionen mit `is_abo_addon = 1`. Die Aufteilung ist damit belastbar — die Einschränkung „erst ab 06/2026 erfasst" steht als Tooltip an der Spalte.
> - **Sortierlogik entdoppelt:** Die 80 Zeilen Sortier-/Such-JavaScript aus der Statistik-Detailseite liegen jetzt in `resources/views/components/_sortable_table.blade.php` und werden von beiden Seiten eingebunden, statt für K8 kopiert zu werden.
> - **Termin-Logik entdoppelt:** `forecastDays()` und `personalCustomerAboForecast()` teilen sich `cumulateByDeliveryDay()` — es gibt weiterhin nur eine Stelle, die die vier Liefertermine kumuliert.
> - **Yajra vs. clientseitig:** K8 nutzt bewusst das vorhandene clientseitige Sortier-Muster statt Yajra. Grund: „Anzahl Ausführungen" (`getCountPaidOrders()`) und „Abo-Punkte" (`getTotalPoints()`) sind in PHP berechnet und in SQL nicht sortierbar — genau zwei der drei Sortierungen, die die Kundin nennt. Die Datenmenge (eigene Kundenabos eines Beraters) rechtfertigt kein Serverpaging.
>
> **Tests:** 10 neue Tests (Aufteilung der Abo-Punkte inkl. Altdatenfall, deduplizierte Recruiter-Summe, Telefon mit Mobil-Fallback, persönliche Liefertag-Prognose, Rendering der Kundenabo-Liste mit und ohne Prognose). Gesamtsuite: 987 grün gegen unveränderte Baseline von 92 vorbestehenden Fehlern (Payone-Race, DATEV, `payment.php`-Sprachkeys aus dem Stripe-Commit).
>
> **Beim Deploy:** `php artisan cache:clear`. K1, K2, K6 und K7 ändern den Snapshot-Payload — abgeschlossene Monate zeigen die neuen Spalten erst nach einer Neuberechnung der Snapshots; bis dahin greifen die Fallbacks („–" bzw. keine Summenzeile).
| # | Paket | Bereich | Einstufung | Aufwand |
|---|---|---|---|---|
| **A1** | Abo pausieren/stornieren nach der 6. Lieferung | Abo | **läuft bereits** — nur Antwort | — |
| **A2** | Payline-Prognose-Punkte stimmen | Home | **bestätigt** — nur Antwort | — |
| K1 | Kundenabos (Umsatz): Punkte in 3 Spalten aufteilen | VIP-Statistik | Erweiterung | klein–mittel |
| K2 | Führungskräfte: „direkt"-Spalten entfernen | VIP-Statistik | Rückbau | klein |
| K3 | Payline-Prognose: Beschriftung der Basis | Home | Mini-Änderung | sehr klein |
| K4 | Team-Abos Payline: Summe zusätzlich ausweisen | Team-Abos | Mini-Änderung | sehr klein |
| K5 | Punkteübersicht: Abo- vs. Shop-Bestellung markieren | Team-Punkte | kleine Änderung | klein |
| K6 | Neupartner-Statistik: Telefon + Spalten verständlich machen | VIP-Statistik | Erweiterung | klein |
| K7 | Recruiter des Monats: Summenzeile | VIP-Statistik | Erweiterung | klein |
| K8 | Eigene Kundenabos als sortierbare Liste | Portal/Abo | Umbau der Ansicht | mittel |
| K9 | Persönliche Liefertag-Prognose 5./10./20./25. | Portal/Abo | neue Kennzahl | mittel |
| K10 | „Die besten Ergebnisse" firmenweit statt teamintern | Home | **Grundsatzentscheidung** | mittel |
| K11 | Statistik und Struktur zur Deckung bringen | VIP-Statistik | **Grundsatzentscheidung** | groß |
| K12 | Deaktivierte Berater nach 2 Wochen zu Kunden des Sponsors | Cron/Prozess | Verhaltensänderung | groß |
| K13 | Jahresgebühr automatisch verlängern und abbuchen | Payment/Prozess | neues Feature | groß |
| K14 | Kundenabo → Partnerabo per Button im Account | Portal/Abo | neues Feature | groß |
| K15 | Neupartner-Registrierung: rabattierte Zusatzprodukte | Registrierung | neues Feature | groß |
**Empfohlene Reihenfolge:** A1/A2 beantworten → **K3 · K4 · K2 · K7 · K6 · K5** (Quick-Wins, ein Release) → **K1 · K8 · K9** → **K10** (nach Anwaltsfreigabe) → **K11** (eigene Runde, viel Abstimmung) → **K14 · K15 · K12 · K13** (Prozess-Pakete, je einzeln planen).
---
## A1 — Abo pausieren/stornieren: läuft bereits automatisiert
**Ihre Frage:** *„Wie funktioniert aktuell das Pausieren, stornieren bei den Abos? Ausgemacht war: Nach der 6. Lieferung kann man das Abo 1x in 6 Monaten pausieren / monatlich stornieren (das sollte alles automatisiert laufen, läuft es aber meines Wissens noch nicht)."*
**Befund: Es läuft — und zwar genau nach diesen Regeln.** Kein Entwicklungsbedarf.
- [AboHelper::canCancelAbo()](../../app/Services/AboHelper.php#L125-L134): Kündigung erst ab `getCountPaidOrders() >= abo-min-duration`. **Setting steht produktiv auf 6** — also ab der 6. bezahlten Lieferung, danach jederzeit.
- [AboHelper::canPauseAbo()](../../app/Services/AboHelper.php#L163-L179): Pause verlangt zusätzlich, dass die letzte Pause mindestens `abo-pause-interval-months` zurückliegt (rollierend ab `last_pause_date`). **Effektiv 6 Monate.**
- Zusätzlich greift eine Sperrfrist von **3 Tagen** vor der Ausführung (`abo-lock-days-pause-cancel`), damit eine Pause nicht in eine schon angestoßene Lieferung fällt.
**Eine Kleinigkeit zum Aufräumen (5 Minuten):** Das Setting `abo-pause-interval-months` ist in der Datenbank **nicht gepflegt**; der Wert 6 kommt aus der Code-Konstante `DEFAULT_PAUSE_INTERVAL_MONTHS`. Funktional identisch, aber im Admin nicht sichtbar — sollte explizit auf 6 gesetzt werden, damit die Regel nachlesbar ist.
## A2 — Payline-Prognose: Zahlen bestätigt
**Ihre Rückmeldung:** *„Die Prognose-Punkte stimmen."* — Deckt sich mit unserer Gegenprüfung. Übernehmen wir so, keine Änderung. Die gewünschte Beschriftung ist K3.
---
## K1 — Kundenabos (Umsatz): abgerechnete Punkte in 3 Spalten
**Briefing:** Statt einer Spalte „Abgerechnet (Punkte)" drei Spalten: **Abgerechnet** · **davon reine Abo-Punkte** · **einmalige Punkte zum Abo dazu**. Beispiel: *Tanja Longerich 1231 abgerechnet – davon 1000 reine Abo-Punkte – 231 einmalige Bestellungen dazu.* Nur bereits abgerechnete Abos.
**Ist-Stand:** [statistics/index.blade.php:627](../../resources/views/user/backoffice/statistics/index.blade.php#L627) zeigt eine Spalte „Abgerechnet (Punkte)" aus [`customerAboActualPoints()`](../../app/Services/Backoffice/BackofficeDashboardService.php#L799-L811). Die Summe kommt aus `user_sales_volumes` gefiltert auf `shopping_orders.is_abo = 1` ([SalesVolumePoints::customerAboPoints()](../../app/Services/Backoffice/SalesVolumePoints.php)) — **auf Bestellebene**, also Abo-Positionen und Einmal-Zusätze in einem Topf.
**Gute Nachricht:** Der Marker existiert bereits. `shopping_order_items.is_abo_addon` (Migration `2026_06_05_120100`) unterscheidet Abo-Positionen von Einmal-Zusätzen und wird an allen drei Schreibpfaden gesetzt — [CheckoutRepository](../../app/Repositories/CheckoutRepository.php#L154), [PaymentHelper](../../app/Services/PaymentHelper.php#L133) und dem Abo-Cron [UserMakeOrder](../../app/Cron/UserMakeOrder.php#L315). `shopping_order_items.points` gibt es ebenfalls.
**Umsetzung:** Zusätzliches Aggregat über `shopping_order_items`, join auf die Abo-Bestellungen des Monats, gruppiert nach `is_abo_addon`. Zwei neue Werte je Berater (`abo_points_recurring`, `abo_points_addon`); die bestehende Spalte bleibt als Summe stehen und muss weiterhin aus `user_sales_volumes` kommen, damit sie mit den übrigen Punkte-Kennzahlen konsistent bleibt.
> **Wichtige Einschränkung, die in die Kundenantwort gehört:** `is_abo_addon` gibt es erst **seit dem 05.06.2026**. Für Bestellungen davor ist der Wert `false` — die Aufteilung zeigt dort alles als „reine Abo-Punkte". Für Mai und früher ist die Spalte also nicht aussagekräftig. Entweder so kennzeichnen oder die Aufteilung erst ab 06/2026 anzeigen (Empfehlung: kennzeichnen, nicht ausblenden).
>
> **Zweite Prüfung vor der Umsetzung:** Ob die Summe der Positionspunkte je Bestellung exakt den `user_sales_volumes.points` derselben Bestellung entspricht, muss an echten Daten gegengerechnet werden (Rundung, Kompensationspositionen `comp`, Gutschriften). Weicht sie ab, zeigen wir die Aufteilung als *Anteile* der bestehenden Summe statt als eigenständige Zahlen.
## K2 — Führungskräfte: „direkt"-Spalten entfernen
**Briefing:** *„Die Spalte (Teamabos direkt) und (Team-Kundenabos direkt) braucht es nicht!"*
Damit ist die **offene Frage 3 aus dem August-Brief beantwortet** — sie wollte damals beide Sichten sehen, jetzt reicht „gesamt".
**Ist-Stand:** [statistics/index.blade.php:386-390](../../resources/views/user/backoffice/statistics/index.blade.php#L386-L390) zeigt 4 Team-Spalten (`team_abo_count`, `team_customer_abo_count`, `..._total`), [`leaderAboOverview()`](../../app/Services/Backoffice/BackofficeDashboardService.php#L591-L640) berechnet beide Sichten über zwei Durchläufe von `teamIdsPerLeader()`.
**Achtung — ein Detail, das leicht übersehen wird:** Die **Kopf-Summe** über der Tabelle (`total_team` / `total_team_customer`) ist bewusst aus den **direkt**-Werten gebildet, weil nur die überschneidungsfrei sind. Fallen die Spalten weg, muss entschieden werden, was die Kopfzahl künftig zeigt:
- **(a)** Kopf-Summe bleibt intern auf der direkt-Rechnung (korrekt, aber die Zahl lässt sich dann nicht mehr aus der Tabelle nachrechnen), oder
- **(b)** Kopf-Summe entfällt ebenfalls.
Empfehlung: **(a)** mit einer Fußnote „Summe überschneidungsfrei gerechnet". → **Rückfrage F1.**
**Umsetzung:** Spalten und Fußnotentext aus der View entfernen, `teamIdsByLeader`-Zweig in `leaderAboOverview()` nur noch für die Kopf-Summe rechnen (spart einen Durchlauf). Der Drilldown auf die entfernten Badges muss mit raus.
## K3 — Payline-Prognose: Basis beschriften
**Briefing:** *„Aber ich würde dazuschreiben: Paylinepunkte + persönliches Volumen, dann kennt man sich aus."*
**Ist-Stand:** Die Basis der vier Kacheln ist in [HomeController](../../app/Http/Controllers/HomeController.php#L117-L121) genau das: `customerPoints + paylinePoints`, wobei `customerPoints = getPointsKPSum()` (persönliches Volumen inkl. Shop). Die Beschriftung fehlt nur.
**Umsetzung:** Sprachkey `home.payline_forecast_title` um den Zusatz ergänzen bzw. eine Unterzeile „Basis: Paylinepunkte + persönliches Volumen" — 4 Sprachen. **Reine Textänderung.**
## K4 — Team-Abos Payline: Summe ausweisen
**Briefing:** *„Wenn da steht 149 Teamabos – 391 Kundenabos in der Payline, wäre es schön, wenn die Summe der Abos in der Payline auch noch da steht."*
**Ist-Stand:** [abos.blade.php:70](../../resources/views/user/team/abos.blade.php#L70) rendert `abo.payline_abo_counts` mit `team_abo_count` und `team_customer_abo_count` aus [`PaylineForecastService::aboTurnover()`](../../app/Services/Abo/PaylineForecastService.php). Beide Werte liegen vor, die Summe wird nur nicht gebildet.
**Umsetzung:** Sprachkey um `:total` erweitern, in der View summieren. **Reine Textänderung**, 4 Sprachen.
## K5 — Punkteübersicht: Abo- gegen Shop-Bestellung absetzen
**Briefing:** *„Hier wäre es noch hilfreich zu sehen, welcher dieser Bestellungen eine ABO-Bestellung ist (statt Shop-Bestellung – Abo-Bestellung und in einer anderen Farbe)."*
**Ist-Stand:** [TeamController::datatablePoints()](../../app/Http/Controllers/User/TeamController.php#L1084-L1127) rendert die Spalte „Bestellung" als Badge — heute unterscheidet die Farbe nur *eigene* (`btn-primary`) von *Kunden*-Bestellungen (`btn-secondary`). `shopping_orders.is_abo` ist über die geladene Relation direkt verfügbar.
**Umsetzung:** Badge-Farbe und Beschriftung um die Abo-Unterscheidung erweitern (z. B. Abo = `btn-info` mit Zusatz „Abo"), Legende über der Tabelle. Klein — aber die Yajra-Sortierung auf der Spalte muss weiterhin funktionieren.
## K6 — Neupartner-Statistik: Telefon + Spalten verständlich machen
**Briefing:** Zwei Punkte:
1. *„Hier würde ich auch die Telefonnummer des Neupartners hinzufügen."*
2. *„Was bedeutet die Spalte Kundenpunkte vs Punkte gesamt? Das ist für mich nicht schlüssig."*
**Ist-Stand:** [`newPartnerRows()`](../../app/Services/Backoffice/BackofficeDrilldownService.php#L133-L164) liefert Name, E-Mail, Karriere-Level, Sponsor, `customer_points`, `total_points`, aktiv seit, Konto bis. **Telefon fehlt** — steht aber in `user_accounts.phone` / `.mobil` und wird über `with('account')` bereits mitgeladen.
Zu 2.: Die Spalten sind korrekt, aber schlecht benannt. **Kundenpunkte** = Punkte aus Bestellungen *seiner Kunden* (Shop + Abo). **Punkte gesamt** = Kundenpunkte **plus seine Eigenbestellungen**, und da steckt vor allem das **Starterpaket** drin. Genau diese Spalte hatten wir im August auf ihren Hinweis hin ergänzt, damit das Starterpaket sichtbar wird — der Zusammenhang ist aus der Überschrift nicht erkennbar.
**Umsetzung:**
1. Telefon-Spalte ergänzen (mit `phone`-Fallback auf `mobil`, wie auf der Firstlines-Seite), inklusive CSV-Export.
2. Spalten umbenennen: **„Kundenpunkte"** → **„Punkte seiner Kunden"**, **„Punkte gesamt"** → **„Gesamt (inkl. Eigenbestellung/Starterpaket)"**, plus eine Erklärzeile unter der Tabelle. Textänderung, kein Rechenweg-Eingriff.
## K7 — Recruiter des Monats: Summenzeile
**Briefing:** *„Hier kann man am Ende noch eine Summe bilden – das Volumen aller Neupartner zusammen, die in diesem Monat eingestiegen sind."*
**Ist-Stand:** [`recruiterRanking()`](../../app/Services/Backoffice/BackofficeDashboardService.php#L834-L936) liefert Zeilen mit `new_partner_count` und `volume_points`. Der Kopf zeigt bereits die Gesamtzahl der Neupartner; eine Volumen-Summe fehlt.
**Wichtig — nicht einfach die Spalte aufaddieren:** `volume_points` enthält das **Kettenvolumen** (Punkte der Neupartner, die diese Neupartner selbst geworben haben). Wer im selben Monat von A geworben wurde und selbst B wirbt, taucht in A's Kettenvolumen **und** in seiner eigenen Zeile auf — die Spaltensumme zählt B doppelt.
**Umsetzung:** Die Summe muss **einmal je Neupartner** über die deduplizierte Neupartner-Menge gerechnet werden (`SalesVolumePoints::totalPointsByUser()` über alle `newPartnerIds`), nicht als Spaltensumme. Als Fußzeile „Volumen aller N Neupartner des Monats: X Punkte" mit Tooltip, warum das weniger sein kann als die Spalte darüber.
## K8 — Eigene Kundenabos als sortierbare Liste
**Briefing:** *„Eigene Kundenabos sehr unübersichtlich – in Listenform. Kann man das ähnlich anzeigen lassen wie die Teamabos und Teamkundenabos in Listenform? Evtl. mit der Möglichkeit es auch zu sortieren – nach Startdatum, nach Anzahl der Ausführungen, nach Liefertag."*
Gewünschte Spalten laut ihrer Skizze:
`Name des Kunden | Abo-Liefertag | Abo-Punkte | Nächste Ausführung | Start-Datum | Abo-Ausführungen`
**Ist-Stand:** [user/abo/index.blade.php](../../resources/views/user/abo/index.blade.php) rendert **eine Karte pro Abo** (267 Zeilen Karten-Markup) — bei vielen Kundenabos entsteht genau die beschriebene Unübersichtlichkeit. Die Daten liegen alle vor: `shopping_user` (Kundenname), `abo_interval` (Liefertag), `getTotalPoints()`, `next_date`, `start_date`, `getCountPaidOrders()`.
**Umsetzung:** Tabellenansicht für `view = 'ot'` als Standard, Karten als Detail hinter dem Klick (Detailseite existiert: `user_abos_detail`). Sortierung projektkonform über **Yajra Datatables** (CLAUDE.md: Listenansichten ausschließlich Yajra) — dafür braucht es eine Datatable-Route analog zu `user_team_datatable_points`. Der bestehende Monatsfilter und die Chart-Karte bleiben darüber stehen.
> **Achtung:** Die Karten enthalten heute die Sonderfälle „Zahlungslink offen", „Erstzahlung ausstehend/fehlgeschlagen" mit Aktionsbuttons. Die dürfen nicht verloren gehen — entweder als Statusspalte mit Badge in der Liste oder als Kartenblock **über** der Tabelle (Empfehlung: Letzteres, da es Ausnahmefälle sind).
## K9 — Persönliche Liefertag-Prognose 5./10./20./25.
**Briefing:** *„Hier wäre es auch noch interessant zu sehen, wieviel Volumenpunkte habe ich am 5. / 10. / 20. / 25. Nur mit meinen Kundenabos."*
**Ist-Stand:** [`PaylineForecastService::forecastDays()`](../../app/Services/Abo/PaylineForecastService.php) rechnet genau diese vier Termine — aber für die **Payline** (Team) und mit dem persönlichen Umsatz als Basis. Eine Variante „nur meine eigenen Kundenabos" gibt es nicht.
**Umsetzung:** Zweite, schlanke Methode auf demselben Baustein: gleiche Termin-Logik (`UserAbo::$aboDeliveryDays`, kumuliert, `next_date` im laufenden Monat), aber Mitgliedermenge = nur der Berater selbst, `is_for = 'ot'`. Anzeige auf der neuen Kundenabo-Liste aus K8 als vier kleine Kacheln — das passt inhaltlich zusammen und sollte **mit K8 in einem Zug** gebaut werden.
> **Klärung:** Sie schreibt „nur mit meinen Kundenabos" — also **ohne** das eigene Abo. Das ist die umgekehrte Entscheidung zur Payline-Prognose, wo eigene Abos bewusst mitzählen (Frage 2 aus dem August-Brief, von ihr noch nicht beantwortet). Wir bauen es wie beschrieben (nur Kundenabos), sollten den Unterschied aber benennen. → **Rückfrage F2.**
## K10 — „Die besten Ergebnisse" firmenweit statt teamintern
**Briefing:** *„Das ist sehr toll umgesetzt, doch es wäre sinnvoller dies nicht für das eigene Team zu machen, sondern für die ganze Firma – das müsste ja jetzt auch mit dem Anwalt geklärt sein?"*
**Ist-Stand — dafür vorgesehen:** [`TopResultsService::SCOPE`](../../app/Services/Team/TopResultsService.php#L30) ist genau als dieser Schalter angelegt (`'team'` / `'company'`), und [`scopedUserIds()`](../../app/Services/Team/TopResultsService.php#L78-L84) ist die einzige Stelle, die sich ändern muss. Die Ranglisten-Logik bleibt unangetastet.
**Was trotzdem zu tun ist — der Schalter allein reicht nicht:**
1. **Performance/Cache.** Teamintern wird je Betrachter gecacht (`top_results_{user}_{month}_{year}`). Firmenweit gibt es **ein** Ranking für alle — der Cache-Key muss den User verlieren, sonst rechnen wir dasselbe Ergebnis 900-mal. Gleichzeitig läuft die Rangliste dann über alle Berater statt über eine Downline; das ist als eine gebatchte Query unkritisch, aber die Kandidaten-Hydrierung (`TOP_LIMIT + 22`) muss geprüft werden.
2. **„ich"-Markierung** bleibt, muss aber nach dem Cache gesetzt werden (wie heute schon die Profilfotos), sonst sieht jeder das „ich" des Ersten, der die Seite geladen hat. **Das ist der Fehler, den man hier am leichtesten baut.**
3. **Datenschutz.** Firmenweit sieht jeder Berater Namen, Fotos und Umsatzhöhe fremder Berater ohne Vertriebsbeziehung. Das ist eine andere Qualität als teamintern und braucht die Freigabe, die sie selbst anspricht — inklusive des Einwilligungstexts fürs Profilfoto, der **weiterhin ein Platzhalter** ist (offener Punkt aus dem August-Brief).
→ **Rückfrage F3: schriftliche Bestätigung der Anwaltsfreigabe + finaler Einwilligungstext.** Ohne beides würde ich das nicht scharf schalten.
## K11 — Statistik und Struktur zur Deckung bringen
**Briefing:** *„Kann man Statistik und Struktur so anzeigen, damit man die gleiche Summe bekommt – ich würde die Zahlen der Struktur nehmen, denn diese sind für die Provisionen relevant!"*
**Befund: Sie hat recht, und wir kennen die Ursache genau.** Die beiden Seiten laufen über zwei verschiedene Baum-Definitionen:
| | Statistik (`user/backoffice/statistics`) | Struktur (`user/team/structure`) |
|---|---|---|
| Baum | [`BackofficeDashboardService::lineBuckets()`](../../app/Services/Backoffice/BackofficeDashboardService.php#L247-L277) | `TreeCalcBotOptimized` → [`readParentsBusinessUsers()`](../../app/Services/BusinessPlan/BusinessUserItemOptimized.php#L1080-L1092) |
| Nimmt auf | **jeden** nicht gelöschten Downline-User | nur `m_level` **und** `payment_account` gesetzt, `admin < 4`, nicht `id = 1`, `active_date <= Monatsende` |
| Bei ausgefiltertem Knoten | Unterbaum wird **weiter** traversiert | Unterbaum wird **komplett gekappt** |
| Max. Tiefe | 100 | 20 |
Gemessen an Claudias Account (08/2026): **578 Personen in der Downline, davon 134 nicht im Provisionsbaum.** Die Punkte-Differenz war an diesem Stichtag zufällig 0, weil die ausgefilterten Konten Blätter ohne Umsatz waren — strukturell ist die Abweichung aber jederzeit möglich.
**Vorarbeit ist schon geleistet:** Für die Level-/Provisions-Prognose wurde diese Angleichung am 01.09. bereits gebaut — [`PaylineForecastService::lineByUserId()`](../../app/Services/Abo/PaylineForecastService.php) filtert und kappt jetzt nach denselben Regeln, inklusive Regressionstest. **Dasselbe Prädikat lässt sich wiederverwenden**; es gehört dann als gemeinsame Definition in den `BackofficeDashboardService`.
**Warum das trotzdem groß ist:** `lineBuckets()` ist die Grundlage von **allem** in der VIP-Statistik und auf dem Home-Dashboard — Berater-Zahlen, Neupartner, Abo-Bestände, Punkte, Payline-Mitglieder, Top-3, Führungskräfte, Recruiter. Eine Umstellung ändert praktisch jede Zahl auf beiden Seiten, und zwar auch rückwirkend in der Anzeige.
**Umsetzungsvorschlag in drei Schritten:**
1. **Erst messen, dann umstellen.** Ein Auswertungs-Command, der für alle VIPs beide Baum-Definitionen gegenüberstellt (Anzahl Berater, Neupartner, Abos, Punkte je Ebene). Damit weiß man vor der Umstellung, wo und wie stark sich Zahlen bewegen — und kann es der Kundin vorher sagen, statt sie es entdecken zu lassen.
2. **Gemeinsames Prädikat** aus `PaylineForecastService` in den `BackofficeDashboardService` heben, `lineBuckets()` bekommt die Provisionsbaum-Variante als zweite Methode.
3. **Umstellen mit Stichtag**, gespeicherte Snapshots neu berechnen, und in der Kundenmitteilung offenlegen, welche Zahlen sich warum geändert haben — analog zur Neuberechnung im August.
> **Ein Punkt, den sie kennen sollte:** Die Statistik zählt heute bewusst auch Berater mit **abgelaufener** Jahresgebühr als „Berater" (Nachfass-Perspektive: sie sollen auffindbar bleiben). Der Provisionsbaum tut das nicht. Nach der Umstellung verschwinden diese Personen aus der Statistik — genau die, für die wir im August die Jahresgebühr-Liste gebaut haben. → **Rückfrage F4:** Provisionssicht überall, oder Provisionssicht als Standard mit umschaltbarer „inkl. abgelaufene"-Ansicht? Ich empfehle Letzteres.
## K12 — Deaktivierte Berater nach 2 Wochen zu Kunden des Sponsors
**Briefing (sie kennzeichnet ihn selbst als „Vorschlag, den wir noch besprechen müssen"):** Bei Deaktivierung **innerhalb von 2 Wochen, nicht 2 Monaten**: Berater wird als Kunde geführt, seine Kunden **und er selbst** gehen als Kunden an den Sponsor, seine Teampartner werden neue Firstlines des Sponsors. Sponsor bekommt eine Mail mit beiden Listen (Vorname, Nachname, Telefon, E-Mail). 2 Wochen vorher eine Mail an den Berater mit der Konsequenz.
**Ist-Stand — die Logik existiert komplett, nur zum falschen Zeitpunkt.** [`UserCleanUp`](../../app/Console/Commands/UserCleanUp.php) läuft täglich um 03:30 in zwei Stufen:
| Stufe | Zeitpunkt | Was passiert |
|---|---|---|
| `cleanUpInActiveUser()` | payment_account **< −2 Wochen** | Teampartner → nächster aktiver Sponsor, User deaktiviert (`pre_sponsor` gemerkt). **Kunden bleiben beim deaktivierten User.** |
| `deleteInactiveUsers()` | payment_account **< −2 Monate** | zusätzlich `setShoppingUserToNewMember()` + `setUserToClient()`, dann Soft-Delete |
Ihr Wunsch = die beiden `UserUtil`-Aufrufe aus der 2-Monats-Stufe in die 2-Wochen-Stufe vorziehen. **Die Bausteine sind fertig** ([UserUtil](../../app/Services/UserUtil.php)), es fehlen nur die Mails.
**Das eigentliche Problem ist nicht technisch, sondern fachlich — Reversibilität:** Die 2-Wochen-Stufe ist heute bewusst **umkehrbar**. `UserUtil::reactiveUserResetChilds()` holt Teampartner über `pre_sponsor` zurück, wenn jemand verspätet zahlt. Zieht man den Kundentransfer und die Kundenumwandlung vor, ist das nach 2 Wochen **endgültig** — wer am 15. Tag zahlt, bekommt seine Kunden nicht zurück.
Sie schreibt selbst: *„Wenn ein Berater rausgefallen ist, muss er sich wieder mit Starterpaket registrieren… seine Teampartner und Kunden sind dann aber schon seinem Sponsor übergegangen."* — das klingt nach bewusster Akzeptanz. Bei 2 Wochen Frist und einer Jahresgebühr halte ich das trotzdem für hart und rechtlich nicht ohne. → **Rückfrage F5.**
**Umsetzung (wenn bestätigt):**
1. `cleanUpInActiveUser()` um `setShoppingUserToNewMember()` + `setUserToClient()` erweitern; Reaktivierungspfad entsprechend anpassen oder bewusst entfernen.
2. **Neue Mail an den Sponsor** mit beiden Listen — Datenquelle sind exakt die im selben Transaktionsblock umgezogenen IDs, nicht ein nachträglicher Query.
3. **Vorwarn-Mail an den Berater 2 Wochen vor Ablauf.** Achtung: [`CheckPaymentsAccount`](../../app/Console/Commands/CheckPaymentsAccount.php) verschickt bereits Erinnerungen — die neue Mail muss sich dort einreihen statt eine zweite Mailstrecke aufzumachen.
4. **Alles idempotent und transaktional**, mit Trockenlauf-Modus. Ein fehlerhafter Lauf verschiebt Kundenbeziehungen unwiederbringlich.
## K13 — Jahresgebühr automatisch verlängern und abbuchen
**Briefing:** *„Jahresgebühr: diese wird automatisch verlängert und abgebucht. Falls man sie nicht verlängern möchte, muss man eingreifen und nicht, wenn man verlängern möchte!"*
**Ist-Stand:** Heute ausdrücklich **manuell** — der Sprachkey sagt es dem Berater wörtlich: *„Sobald Deine Zahlung bei uns eingegangen ist, verlängert sie sich um ein weiteres Jahr. **Eine automatische Verlängerung gibt es nicht.**"* ([membership.php:36](../../resources/lang/de/membership.php#L36)).
**Interessanter Fund:** Es existieren bereits fertige Texte für genau dieses Feature — [payment.php:15-17](../../resources/lang/de/payment.php#L15-L17) beschreibt SEPA-Mandat, Speicherung und Abbuchung **14 Tage vor Ablauf** inklusive jederzeitigem Widerruf. Diese Keys werden **nirgends** verwendet, und es gibt weder ein Feld noch einen Cron dafür. Das Feature war offenbar konzipiert und nicht gebaut.
**Umfang (bewusst grob, das braucht eine eigene Planungsrunde):** Opt-out-Kennzeichen am Account, gespeichertes Zahlungsmittel/SEPA-Mandat (PAYONE bzw. Stripe — beide Wege existieren im System), Cron „14 Tage vor Ablauf abbuchen", Fehler-/Retry-Behandlung bei Rücklastschrift, Rechnungserzeugung, Kündigungsweg im Account, sowie AGB/Widerruf. **Rechtlich ist die Umkehrung von Opt-in auf Opt-out der kritische Teil** — für Bestandsberater lässt sich ein Lastschriftmandat nicht rückwirkend annehmen. → **Rückfrage F6:** gilt das nur für Neuregistrierungen, oder sollen Bestandsberater aktiv zustimmen?
## K14 — Kundenabo → Partnerabo per Button im Account
**Briefing:** Wenn ein Kunde mit aktivem Abo Partner wird, musste bisher eine Mail an mivita gehen, damit das Abo gestoppt und in ein Partnerabo umgewandelt wird. Gewünscht: Button im Account — *„jetzt Kundenabo in Partnerabo ändern, damit du auch dein ABO 30 % günstiger bekommst"*, dann ein Vorschlag aus dem Kundenabo plus die Möglichkeit, Kompensationsprodukte hinzuzufügen, mit einem Feld für die Begründung.
**Ist-Stand:** Kein Umwandlungspfad im Code — der Vorgang läuft heute tatsächlich manuell. Vorhanden und wiederverwendbar sind: die Abo-Anlage über `AboOrderCart`, die Kompensationslogik (`comp`-Flag auf `user_abo_items`, wird in allen Punkte-Abfragen bereits berücksichtigt) und das Einmalprodukt-Muster [`AboOneTimeService`](../../app/Services/AboOneTimeService.php) als Vorlage für „Vorschlag + Positionen ergänzen".
**Fachliche Knackpunkte, die vor der Umsetzung geklärt sein müssen:**
- Läuft das alte Kundenabo aus oder wird es storniert — und was passiert mit den bereits gezählten **Ausführungen** (die 6-Lieferungen-Regel aus A1 beginnt sonst von vorn)?
- Der Kunde hing als `shopping_user` an einem Berater. Wird er selbst Partner, wandert der Umsatz von dessen Kundenpunkten in seine Eigenpunkte — das verschiebt Provisionen und Statistik.
- Kompensationsprodukte mit Begründungsfeld heißt: es braucht eine Freigabe (mivita bzw. Admin), sonst kann sich jeder Partner beliebig Kompensation eintragen.
→ **Rückfrage F7.** Ohne diese drei Antworten würde ich nicht anfangen.
## K15 — Neupartner-Registrierung: rabattierte Zusatzprodukte
**Briefing:** *„Die Möglichkeit gleich bei der Registrierung noch weitere Produkte rabattiert dazuzubestellen (bei Starterpaket -30 %; ohne Starterpaket -20 %), wenn jemand z. B. das Basic Health Paket auswählt und noch eine Aer Sun dazubestellen möchte."*
**Ist-Stand:** In der Registrierungsstrecke gibt es keinen Weg, dem Startpaket weitere Produkte hinzuzufügen. Die Rabattstaffel 30 %/20 % ist die reguläre Berater-Kondition; die Preislogik existiert also, nur nicht an dieser Stelle.
**Zu klären:** Punktewirkung der Zusatzprodukte (zählen sie wie das Starterpaket zu den Eigenpunkten des Neupartners und damit ins „neue Volumen" des Recruiters?), Versand (eine Sendung oder zwei), Storno/Widerruf bei gescheiterter Registrierung, und ob der Rabatt schon vor der Freischaltung des Accounts greifen darf. → **Rückfrage F8.**
---
## Rückfragen an die Kundin
| # | Frage | Blockiert |
|---|---|---|
| **F1** | Führungskräfte: Wenn die „direkt"-Spalten entfallen — soll die Kopf-Summe bleiben (überschneidungsfrei gerechnet, aber nicht mehr aus der Tabelle nachrechenbar) oder ebenfalls weg? | K2 |
| **F2** | Persönliche Liefertag-Prognose „nur mit meinen Kundenabos" — **ohne** das eigene Abo, richtig? (In der Payline-Prognose zählen die eigenen Abos bewusst mit.) | K9 |
| **F3** | Firmenweite Ranglisten: Liegt die Anwaltsfreigabe schriftlich vor? Und bitte den **finalen Einwilligungstext fürs Profilfoto** — der steht seit August auf Platzhalter. | K10 |
| **F4** | Statistik = Struktur: Sollen Berater mit **abgelaufener** Jahresgebühr dann überall verschwinden, oder Provisionssicht als Standard mit umschaltbarer „inkl. abgelaufene"-Ansicht? (Empfehlung: umschaltbar — sonst fehlen genau die Leute, für die die Jahresgebühr-Liste gebaut wurde.) | K11 |
| **F5** | Deaktivierung nach 2 Wochen: Soll der Kundentransfer **endgültig** sein? Heute ist die 2-Wochen-Stufe umkehrbar — wer verspätet zahlt, bekommt sein Team zurück. | K12 |
| **F6** | Automatische Jahresgebühr: nur für **Neuregistrierungen**, oder sollen Bestandsberater aktiv ein Lastschriftmandat erteilen? | K13 |
| **F7** | Kundenabo → Partnerabo: (a) zählen die bisherigen Ausführungen für die 6-Lieferungen-Regel weiter? (b) Wer gibt Kompensationsprodukte frei? | K14 |
| **F8** | Registrierungs-Zusatzprodukte: Zählen deren Punkte wie das Starterpaket zu den Eigenpunkten des Neupartners (und damit ins „neue Volumen" des Recruiters)? | K15 |
## Technische Querschnittsthemen
- **Sprachkeys:** Jede Textänderung in **de/en/es/fr** — `LocalizationSyncTest` erzwingt Parität. Betrifft K2–K7.
- **Tests:** Für jedes Paket ein Test, der die Regel festhält, nicht nur das Rendering. Bei K1 und K7 sind es die Rechenwege (Aufteilung bzw. Doppelzählung), bei K11 die Baum-Definition.
- **Snapshots:** K1, K2, K6, K7 und K11 ändern den `BackofficeStatisticsSnapshot`-Payload. Abgeschlossene Monate zeigen sonst die alte Struktur — wie im August müssen die Snapshots nachgerechnet und die Kundin über abweichende Altzahlen informiert werden. Die Views brauchen weiterhin den „älterer Snapshot ohne diese Liste"-Fallback.
- **Caches:** Nach jedem Livegang `php artisan cache:clear` — die 6-h-Caches (`payline_context_*`, `live_business_v3_*`, `top_results_*`, `level_forecast_*`) haben keine Invalidierung. Bei K10 ändert sich zusätzlich der Cache-Key-Zuschnitt.
- **Offen aus dem letzten Code-Review** (unabhängig von diesem Feedback, sollte mitlaufen): Cache-Invalidierungsstrategie statt fester 6 h; Widerspruch Home-Kacheln ↔ VIP-Statistik bei Abo-Beständen vergangener Monate (Kundenentscheid F7 aus dem Juli — passt thematisch zu K11 und sollte in derselben Runde mitentschieden werden); Performance von `monthlyGrowth()`.