Statistik-Änderungen 2026-08-19 (dev/2026-07-22/entwicklungsplan-2026-08-19.md, alle 8 Pakete gemäß beantworteten F-Fragen): - N1 Monatsstatistik: Team-Cluster (Firstlines klickbar -> neue Seite user_team_firstlines, Teampartner gesamt/Payline [nur aktive], Abos Payline mit +N) + Level-Kachel (Quali-Stufe, fehlende Payline-/persönliche Punkte, Karriere-Level); LiveCommissionService-Snapshot v3 reicht Level-/Quali-Daten durch - N2 Abo-Kacheln auf Home monatsbezogen (aboStockCount-Rückrechnung über cancel/deleted/Status-Historie; VIP-Statistik bleibt tagesaktuell) - N3 VIP-Statistik 'Recruiter des Monats' (Neupartner-Definition wie KPI, rekursives Ketten-Volumen als Gesamtpunkte inkl. Starterpaket) + Drilldown/CSV-Spalte 'Punkte gesamt' - N4 Prognose-Block 'Level & Provision laut Prognose (Stand 25.)' mit unverbindlich-Kennzeichnung (ebenengenaue offene Abo-Punkte + TP-Laufstände) - N5 Checkout-Herkunft: Dropdown raus, Pflicht-Freitext (lokalisierte Fehlermeldung) - N6 Berater-Profilfoto (user_accounts.profile_image, Einwilligung als Sprachkey-Platzhalter, DSGVO-Löschung) + teaminterne Top-3-Karte (TopResultsService, Scope-Schalter) - N7 USt-Optionen: 'nicht umsatzsteuerpflichtig' zuerst + '§ 19 UStG'-Textfix - N8 Jahresgebühr-Nachfassliste je Berater (AnnualFeeDueService, überfällig/1-3 Monate, Sidenav) Kontroll-Review (8 Perspektiven) angewendet: next-Level-Fallback im Steady-State (resolveNextLevel — sonst 'Höchstes Level erreicht' für Normalfälle), _calculated-Punktebasis bei mehr Paylines, keine Wiederbelebung deaktivierter Abos in der Monatsrückrechnung, Payline-Abo-Kachel monatskonsistent, Top-3-Nachrücker; Performance: BackofficeDashboardService als Singleton mit lineBuckets-Memo (+flushLineBucketsMemo), level_forecast 6h-Cache; Reuse: User-Scopes firstlinesOf/newPartnerIn, SalesVolumePoints::totalPointsByUser, Slim::storeCroppedImage (3 Upload-Kopien vereinigt), activeAbo-Scope statt Inline-Prädikate, Form::open-Konvention, Return-Types Repair-Härtung (Kundenentscheide): VP-Nachtrepair dedupliziert über shopping_order_id (Live-Pfad schreibt sie jetzt mit), zählende Alt-Zeilen-Erkennung, Vormonats-Scan für Monatswechsel, Firmenaccount-Buchungen (config mivita.company_user_id) werden automatisch auf den Berater umgebucht (in place bzw. per Gegenbuchung bei geschlossenem Monat; Echtfall Anna Schall Order 58797 -> Christina Schuder), Berater-zu-Berater nur Warnung; Incentive-Repair: m_sponsor=NULL löscht nicht mehr, FK-Repair vor Stale-Bereinigung, gebatchte Lookups, zentrales Incentive::qualificationWindow(); .env aus dem Git-Tracking entfernt (Pflege per SFTP) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
190 lines
26 KiB
Markdown
190 lines
26 KiB
Markdown
# Entwicklungsplan — Statistik-Änderungen vom 2026-08-19
|
||
|
||
Basis: `dev/2026-07-22/Statistik-Aenderungen-19-08-2026.pdf` (Kundin, Folge-Feedback zur Umsetzung vom 2026-07-24).
|
||
Ist-Stand-Analyse: 2026-08-19.
|
||
|
||
> **Umsetzungsstand (2026-08-19): alle 8 Pakete gemäß den beantworteten F-Fragen umgesetzt** — Details je Paket im Abschnitt „Umgesetzt". Zusammenfassung:
|
||
> - **N7** ✅ USt-Optionen getauscht (Werte unverändert), Text „§ 19 UStG" repariert (de/fr).
|
||
> - **N5** ✅ Checkout: Dropdown raus, Freitext Pflichtfeld; Altdaten/Marker kompatibel; Admin-Fallback-Kette. Tests: `CheckoutOrderSourceValidationTest`.
|
||
> - **N8** ✅ Neue Seite „Jahresgebühr" für **jeden Berater** (F8: eigenes Team): `AnnualFeeDueService` + Route `user_team_annual_fee` + Sidenav-Eintrag, Gruppen überfällig/≤1/≤2/≤3 Monate. Tests: `AnnualFeeDueServiceTest`.
|
||
> - **N1** ✅ Monatsstatistik: neues „Team"-Cluster (Firstlines klickbar → neue Seite `user_team_firstlines`, Teampartner gesamt [nur aktive, F1], Teampartner Payline [aktive, konsistent], Abos Payline mit „+N neu") + Level-Block (Quali-Stufe des Monats, fehlende Payline-/persönliche Punkte, Karriere-Level als Zusatz, F2). Basis: `LiveCommissionService`-Snapshot v2 reicht jetzt `qual_level`/`next_level`/`payline_points_qual_kp`/`personal_points` durch (Cache-Key `live_business_v2_*`).
|
||
> - **N2** ✅ Abo-Kacheln monatsbezogen: `TeamAboOverviewService::aboStockCount()` rechnet für vergangene Monate den Bestand zum Monatsende zurück (start/cancel/deleted + Status-Historie); laufender Monat bleibt live; VIP-Statistik bleibt tagesaktuell (F3). Näherung vor 07/2026 dokumentiert.
|
||
> - **N3** ✅ „Recruiter des Monats" auf der VIP-Statistik (Neupartner-Definition wie KPI, F4a) mit rekursivem Ketten-Volumen als **Gesamtpunkte** (Starterpaket zählt damit); aufklappbare Detailzeilen; Neupartner-Drilldown + CSV zusätzlich mit Spalte „Punkte gesamt" (F4b).
|
||
> - **N4** ✅ Prognose-Block „Level & Provision laut Prognose (Stand 25.)" mit „unverbindlich"-Kennzeichnung (F5): `PaylineForecastService::levelAndCommissionForecast()` — Ebenen-aufgelöste offene Abo-Punkte + heutige TP-Laufstände, Level-Erreichung inkl. persönlichem Volumen, Provisionsschätzung mit den Sätzen des laut Prognose erreichten Levels (Shop/WB fortgeschrieben).
|
||
> - **N6** ✅ Profilfoto (`user_accounts.profile_image`, Upload mit Einwilligungs-Checkbox im Profil, DSGVO-Löschung integriert; Einwilligungstext als Sprachkey-Platzhalter — **finalen Text liefert die Kundin nach**, F7c) + Karte „Die besten Ergebnisse des Monats" **teamintern** (F7a) auf dem Home-Dashboard: Top 3 persönliches Volumen / Neupartner / neue Abos (Kundenabos + Eigenabo, F7b) mit Foto bzw. Initialen-Avatar, 6-h-Cache, Scope-Schalter (`TopResultsService::SCOPE`) für eine spätere firmenweite Variante. Tests: `TopResultsServiceTest`.
|
||
|
||
## Wie ich das Briefing verstehe
|
||
|
||
Das Feedback betrifft diesmal überwiegend die **Monatsstatistik-Card auf dem Home-Dashboard** (für alle Berater), dazu drei Punkte außerhalb der Statistik (Checkout-Herkunftsfeld, Steuerdaten-Formular, Jahresgebühr-Nachfassliste). Es zerfällt in 8 Pakete:
|
||
|
||
| # | Paket | Bereich | Einstufung | Aufwand |
|
||
|---|---|---|---|---|
|
||
| N1 | Monatsstatistik: 7 neue Kennzahlen (Firstlines klickbar, Teampartner, Payline-Zahlen, Level) | Home | Erweiterung | mittel |
|
||
| N2 | Abo-Kacheln beim Monatswechsel monatsbezogen statt eingefroren | Home | Verhaltensänderung | mittel |
|
||
| N3 | „Recruiter des Monats" — Neupartner-Ranking je Sponsor inkl. Volumen + Starterpaket | VIP-Statistik | neue Liste + Zählweise-Fix | mittel |
|
||
| N4 | Prognose: „fehlen zum nächsten Level X Punkte" + voraussichtliche Provision | Home | Erweiterung, **komplexester Punkt** | groß |
|
||
| N5 | Checkout-Herkunft: Dropdown raus, nur Freitext | Shop-Checkout | kleine Änderung | klein |
|
||
| N6 | Teampartner-Foto + „Die besten Ergebnisse des Monats" (3× Top 3) | Home + Profil | neues Feature | mittel–groß |
|
||
| N7 | Steuerdaten: Reihenfolge der USt-Optionen tauschen | Profil/Wizard | Mini-Änderung | sehr klein |
|
||
| N8 | Jahresgebühr-Nachfassliste (fällig in 3/2/1 Monaten + überfällig) | VIP-Statistik | neue Liste | klein–mittel |
|
||
|
||
Empfohlene Reihenfolge: **N7 → N5 → N8 → N1 → N3 → N2 → N6 → N4** (Quick-Wins zuerst; N4 zuletzt, weil er auf N1-Bausteinen aufsetzt und die meisten Klärungen braucht).
|
||
|
||
---
|
||
|
||
## N1 — Monatsstatistik: neue Kennzahlen
|
||
|
||
**Briefing:** Übersichtsliste ergänzen um: Firstlines (Anzahl, klickbar → Liste mit Umsatz, Telefon, Mail), Teampartner insgesamt, Teampartner in der Payline, Abos insgesamt Payline, Neue Abos in der Payline, aktuelles Level, „zum nächsten Level fehlen … Punkte" (+ ggf. fehlendes persönliches Volumen und was die Provision dann wäre).
|
||
|
||
**Ist-Stand (gut vorbereitet):**
|
||
- Firstline-IDs werden im [HomeController.php:127-132](app/Http/Controllers/HomeController.php#L127-L132) bereits berechnet (für „Direkte Neupartner"), die Anzahl wird aber nicht angezeigt.
|
||
- Teampartner gesamt: `BackofficeDashboardService::teamUserIds()` / `activeTeamUserIds()` vorhanden.
|
||
- Payline-Mitglieder + Abo-Zähler: `PaylineForecastService::aboTurnover()` liefert seit Juli `members`, `team_abo_count`, `team_customer_abo_count` — wird vom Home-Dashboard **nur nicht aufgerufen** (nur die Team-Abo-Seite nutzt es).
|
||
- „Neue Abos in der Payline": existiert nicht, aber trivial — `newAboCount()`-Logik auf die Payline-Mitglieder-IDs einschränken (Eigen-Abos über `user_id`, Kundenabos über `member_id`, `start_date` im Monat).
|
||
- Level: `LiveCommissionService::snapshot()` berechnet die Quali-Daten (`qual_user_level`, `next_qual/next_can_user_level` mit `_calculated_*`-Feldern) bereits im Live-Lauf, **wirft sie aber weg** — nur `paylines` wird durchgereicht ([LiveCommissionService.php:117-128](app/Services/BusinessPlan/LiveCommissionService.php#L117-L128)). Eine „Punkte bis zum nächsten Level"-Differenz wird heute nirgends berechnet (auch der Marketingplan zeigt nur die statischen Anforderungen).
|
||
|
||
**Umsetzungsvorschlag:**
|
||
1. Neues KPI-Cluster „Team" auf der Monatsstatistik-Card: Firstlines · Teampartner gesamt · Teampartner Payline · Abos Payline (Teamabos + Kundenabos) · „+N neue Abos Payline" — alle Werte aus vorhandenen Services, gecacht im bestehenden 6-h-Muster.
|
||
2. **Firstlines klickbar:** neue leichte Liste (Modal oder eigene Seite unter `user/team`) für **alle** Berater — Spalten: Name, Karriere-Level, Umsatz (Monats-Gesamtpunkte), Telefon, E-Mail, aktiv seit / Account-Status. Die VIP-Drilldown-Infrastruktur ist VIP-only; für Firstlines (eigene direkte Partner) ist eine eigene, nicht-VIP-Route unbedenklich.
|
||
3. **Level-Kachel:** „Aktuelles Level (Monat): BRONZE — zum nächsten Level SILBER fehlen X Payline-Punkte" + falls die persönliche Anforderung (`qual_kp`) noch nicht erfüllt ist: „dafür fehlen außerdem Y Punkte persönliches Volumen". Rechnung aus dem Snapshot: `nextLevel.qual_pp − payline_points_qual_kp` bzw. `nextLevel.qual_kp − sales_volume_points_KP_sum`. Dazu Anzeige der Provisionssätze des nächsten Levels („dann z. B. Ebene 1–3 je 7 %") — die exakte „was wäre die Provision"-Rechnung gehört zu N4.
|
||
4. Voraussetzung: `LiveCommissionService::buildSnapshot()` um die Level-/Punktefelder erweitern (kein zweiter Baum-Lauf, nur Durchreichen).
|
||
|
||
**Offene Fragen:** F1 (aktive vs. alle Teampartner), F2 (welches „Level" gemeint ist — siehe unten).
|
||
|
||
## N2 — Abo-Kacheln beim Monatswechsel monatsbezogen
|
||
|
||
**Briefing:** Beim Monatswechsel auf der Monatsstatistik passen sich die Kennzahlen an, die 4 Abo-Kacheln (Eigene Abos, Eigene Kundenabos, Teamabos, Team-Kundenabos) aber nicht — sie sollen die Zahlen des gewählten Monats zeigen.
|
||
|
||
**Befund bestätigt:** `TeamAboOverviewService::teamKpis()`/`personalAboKpis()` zählen den **heutigen** `activeAbo`-Bestand; `month`/`year` fließen nur in die „+N neu"-Badges. Auch die Team-Mitgliederbasis ist der heutige Strukturbaum. Es gibt keinerlei Home-Snapshot.
|
||
|
||
**Wichtiger Kontext:** Für die **VIP-Statistik** wurde 2026-07 das **Gegenteil** entschieden (F7: Abo-Bestände immer tagesaktuell, Snapshot wird live überlagert). Der neue Wunsch gilt dem Home-Dashboard — das ist kein Widerspruch, sollte aber bewusst entschieden werden (→ F3).
|
||
|
||
**Umsetzungsvorschlag (Variante A, empfohlen): Rückrechnung über Abo-Daten** — Bestand zum Monatsende M:
|
||
`start_date <= Ende(M)` UND nicht vor Ende(M) beendet (`cancel_date`/`deleted_at`/`user_abo_status_histories` nach Ende(M) oder Abo heute aktiv). Das ist ohne neue Tabellen möglich und für die Vergangenheit eine gute Näherung; seit Juli 2026 protokolliert `user_abo_status_histories` Statuswechsel, wodurch die Rückrechnung ab da exakt wird. Einschränkung dokumentieren: Für Monate vor Juli 2026 können Deaktivierungen ohne `cancel_date` fehlen (Zahlen dann leicht zu hoch).
|
||
**Variante B (verworfen):** monatliche Bestands-Snapshots je User — für ALLE Berater (Home ist nicht VIP-only) unverhältnismäßig teuer.
|
||
Umsetzung: `teamKpis()`/`personalAboKpis()` bekommen einen „Stichtag"-Modus (laufender Monat = live wie bisher; vergangener Monat = Rückrechnung zum Monatsende). Struktur (Team-Mitglieder) bleibt pragmatisch die heutige.
|
||
|
||
## N3 — „Recruiter des Monats" (VIP-Statistik)
|
||
|
||
**Briefing:** Neben „Neue Kundenabos" eine Liste der neuen Teampartner je Sponsor, sortiert nach Anzahl: `Name | Anzahl gesponserte Neupartner | neues Volumen`. Klick auf die Anzahl → wer wurde gesponsert + dessen aktueller Umsatz; hat der Neue selbst schon Neupartner geworben, zählt deren Umsatz dazu. **Und: die Starterpaket-Punkte zählen zum Volumen — „werden jetzt noch nicht gezählt".**
|
||
|
||
**Ist-Stand / Aufklärung Starterpaket:**
|
||
- Ein Sponsor-Ranking existiert nicht; Blaupause ist `newCustomerAboRanking()`.
|
||
- Die Starterpaket-Punkte des Neupartners **werden** systemisch verbucht (Status 1, voll umsatz- und payline-wirksam, 200/400 P. je nach Kit). Was die Kundin vermutlich sieht: Die Spalte „Kundenpunkte" im Neupartner-Drilldown nutzt `externalPoints()` (nur Shop-/Kundenumsatz, Status 2) — die **Eigenbestellung Starterpaket (Status 1) fällt dort raus**. → Fix: im Drilldown und im neuen Ranking „Volumen" als **Gesamtpunkte** (eigene + Kundenpunkte, `totalPoints()`-Logik) rechnen, dann ist das Starterpaket automatisch drin.
|
||
- Zwei konkurrierende Neupartner-Definitionen im System: Statistik (`active_date` im Monat + aktiver Account) vs. Incentive (bezahlte Starterpaket-Bestellung, `created_at`). → F4.
|
||
|
||
**Umsetzungsvorschlag:**
|
||
1. Neue Karte „Recruiter des Monats" auf der VIP-Statistik (Scope: eigenes Team, inkl. „ich" wie bei den anderen Listen): je Sponsor Anzahl Neupartner des Monats + Volumen = Summe der Monats-**Gesamtpunkte** aller seiner Neupartner, **plus rekursiv** die Gesamtpunkte der von diesen Neupartnern im selben Monat geworbenen weiteren Neupartner (Kettenlogik wie im Beispiel der Kundin).
|
||
2. Drilldown (Klick auf Anzahl): Liste der geworbenen Personen mit Monats-Gesamtpunkten, darunter eingerückt deren eigene Neupartner mit Punkten (eine Rekursionsebene sichtbar, Summe vollständig rekursiv).
|
||
3. Nebenbei: Neupartner-Drilldown-Spalte „Kundenpunkte" um „Punkte gesamt (inkl. Starterpaket)" ergänzen oder ersetzen (→ F4b).
|
||
4. Landet mit im Monats-Snapshot (monatsbezogene Liste, kein Live-Overlay nötig).
|
||
|
||
## N4 — Prognose: nächstes Level + voraussichtliche Provision
|
||
|
||
**Briefing:** Bei der Prognose ein Feld „laut Prognose fehlen zum nächsten Level (z. B. Bronze Member) noch 2300 Punkte" und „voraussichtliche Provision laut Prognose: 1200 € (sofern das persönliche Volumen von Y erreicht wird)".
|
||
|
||
**Ist-Stand:** Die Prognose-Kacheln rechnen `basePoints (Kundenumsatz + Payline-Punkte heute) + offene Abo-Punkte bis Termin` — als **eine Gesamtzahl**, ohne Linien-/Ebenen-Zuordnung. Die Provisionsrechnung braucht aber Punkte **pro Ebene** (Sätze `pr_line_1..8` je Level) und die Level-Qualifikation braucht die Trennung „persönliches Volumen (qual_kp)" vs. „Payline-Punkte (qual_pp)".
|
||
|
||
**Umsetzungsvorschlag (zweistufig):**
|
||
1. **Stufe 1 — Level-Prognose (solide machbar):** Die offenen Abo-Ausführungen sind Payline-Mitgliedern zuordenbar (Ebene bekannt aus `lineBuckets`), eigene/eigene Kundenabos zählen aufs persönliche Volumen. Damit: prognostizierte `payline_points_qual_kp` und prognostiziertes persönliches Volumen zum 25. → daraus „laut Prognose fehlen zum nächsten Level X noch N Punkte" bzw. „nächstes Level wird laut Prognose erreicht, sofern das persönliche Volumen von Y P. erreicht wird".
|
||
2. **Stufe 2 — Provisions-Prognose (Näherung, klar kennzeichnen):** prognostizierte Punkte je Ebene × Provisionssätze des laut Prognose erreichten Levels (+ ggf. Wachstumsbonus ausklammern oder mit heutigem Wert fortschreiben). Das ist eine **Schätzung** — Rangverschiebungen im Team, 60%-Regeln etc. bleiben außen vor. Im UI als „voraussichtlich, unverbindlich" kennzeichnen (wie die bestehenden Live-Badges).
|
||
3. Technisch: Erweiterung von `PaylineForecastService` (Ebenen-aufgelöste `upcoming`-Punkte) + `LiveCommissionService` (Level-Daten durchreichen, s. N1.4). Kein neuer Baum-Lauf.
|
||
|
||
**Risiko/Empfehlung:** größter Einzelaufwand des Pakets; erst nach N1 umsetzen und die Genauigkeitserwartung vorher mit der Kundin abstimmen (→ F5).
|
||
|
||
## N5 — Checkout-Herkunft: nur noch Freitext
|
||
|
||
**Briefing:** Statt Dropdown („Sonstiges/Event-Messe sagt uns nichts") nur das Freitextfeld stehen lassen, damit Kunden den konkreten Namen/die konkrete Messe eintragen.
|
||
|
||
**Ist-Stand:** Kunden-Checkout hat Dropdown `customer_order_source` (7 Optionen, Pflicht) **plus optionales Freitextfeld** `customer_order_source_comment` — beides an `shopping_orders` ([checkout.blade.php:556-578](resources/views/web/templates/checkout.blade.php#L556-L578), Optionen in [ShoppingOrder.php:192-200](app/Models/ShoppingOrder.php#L192-L200)). Es gibt **keine** Auswertung/Statistik dazu; Admin zeigt den Wert nur in der Bestell-Detailansicht. Achtung: `CreateAboReplacementOrder` nutzt das Comment-Feld als technischen Marker — Spalten dürfen nicht entfallen.
|
||
|
||
**Umsetzungsvorschlag:** Dropdown aus dem Checkout entfernen, Freitextfeld wird das (einzige) Herkunftsfeld — als Pflichtfeld mit sprechendem Label („Wie bist du auf uns aufmerksam geworden? Bitte konkret: Name der Person, Messe, …"). DB-seitig: `customer_order_source` bleibt bestehen (Bestandsdaten + Marker), neue Bestellungen speichern nur noch den Freitext. Admin-Detailansicht zeigt künftig den Freitext. Aufwand klein. → F6 (Pflichtfeld ja/nein).
|
||
|
||
## N6 — Teampartner-Foto + „Die besten Ergebnisse des Monats"
|
||
|
||
**Briefing:** (a) Feld, in dem jeder Teampartner ein Foto hochlädt; (b) auf der Startseite „Die besten Ergebnisse des Monats": Top 3 persönliches Volumen, Top 3 meiste Neupartner, Top 3 meiste neue Abos — je mit Foto + Name + Wert. Teamintern oder firmenintern ist **laut Kundin noch offen**.
|
||
|
||
**Ist-Stand:** Es gibt **kein** Berater-Profilbild. Das einzige Foto ist das Shop-Bild (`user_shops.filename`, Slim-Cropper-Upload, nur mit eigenem Shop, Anzeige nur im öffentlichen Shop). Ranglisten-Datenquellen existieren teilweise (Neupartner je Sponsor → N3; neue Kundenabos je Berater → vorhandene Liste; persönliches Monatsvolumen je User → `user_sales_volumes`-Aggregat, firmenweit bisher nirgends).
|
||
|
||
**Umsetzungsvorschlag:**
|
||
1. **Profilfoto** als eigenes Feature (unabhängig vom Shop): Spalte an `user_accounts` (oder kleine Tabelle), Upload im Profil (`user_form`/Wizard) mit dem vorhandenen Cropper-Muster aus `UserShopController`, Fallback Initialen-Avatar, DSGVO: Aufnahme in `GdprDeletionService` + Hinweis, dass das Foto team-/firmenintern angezeigt wird (Einwilligung beim Upload).
|
||
2. **Top-3-Karte** auf dem Home-Dashboard: drei Spalten (Volumen / Neupartner / neue Abos), je Top 3 mit Foto, Vor-/Nachname, Wert. Datenlogik: persönliches Volumen = Monats-Gesamtpunkte je Berater; Neupartner = N3-Zählung; neue Abos = neue Kundenabos + neues Eigenabo je Berater (→ F7b). Nächtliche Berechnung/Cache (firmenweit ein Ranking für alle) statt Live-Query pro Aufruf.
|
||
3. **Scope-Schalter teamintern/firmenintern** von Anfang an vorsehen (eine Konstante/Setting), Start laut Rückfrage-Ergebnis (→ F7a).
|
||
|
||
## N7 — Steuerdaten: Reihenfolge der USt-Optionen
|
||
|
||
**Briefing:** „nicht umsatzsteuerpflichtig" soll an erster Stelle stehen.
|
||
|
||
**Ist-Stand:** Hardcodiertes Array in [HTMLHelper.php:399-411](app/Services/HTMLHelper.php#L399-L411) (`1 => umsatzsteuerpflichtig, 2 => nicht umsatzsteuerpflichtig`), gespeichert als `user_accounts.taxable_sales`. Reihenfolge tauschen = Array-Reihenfolge ändern, **Werte bleiben unverändert** (1/2 werden downstream von Gutschriften/DATEV interpretiert, Wert 3 wird intern für Ausland gesetzt — nicht anfassen).
|
||
|
||
**Mitzunehmende Nebenbefunde:** Der Text `taxable_sales_2` ist abgeschnitten und nutzt `$` statt `§` („Kleinunternehmer im Sinne von $ 19") → korrigieren auf „… im Sinne von § 19 UStG)". Aufwand: Minuten. Keine offenen Fragen.
|
||
|
||
## N8 — Jahresgebühr-Nachfassliste
|
||
|
||
**Briefing:** Liste aller Teampartner (komplettes Team), bei denen die Jahresgebühr in 3/2/1 Monaten fällig oder überfällig ist — Spalten Vorname, Nachname, Mail, Telefon, Ebene, Level — „damit wir nachfassen können".
|
||
|
||
**Ist-Stand:** `users.payment_account` = „bezahlt bis"; Verlängerung +1 Jahr bei Zahlung; Erinnerungs-Mails macht der Cron `payments:check-accounts` (21/14/2 Tage vorher), 2 Wochen nach Ablauf deaktiviert `user:cleanup`, nach 2 Monaten wird konvertiert/gelöscht. **Einen „läuft bald ab"-Report gibt es weder für Admin noch Berater** — nur binäre aktiv/inaktiv-Filter in der Team-Liste.
|
||
|
||
**Umsetzungsvorschlag:** Neue Karte/Unterseite auf der VIP-Statistik „Jahresgebühr ausstehend": komplettes Team, gruppiert in **überfällig / fällig ≤ 1 Monat / ≤ 2 Monate / ≤ 3 Monate** (aus `payment_account`), Spalten Vorname, Nachname, Mail, Telefon (aus `user_accounts`), Ebene (Linie aus `lineBuckets`), Karriere-Level, Fälligkeitsdatum; CSV-Export wie bei den anderen Drilldowns. Wichtig: Anzeige „überfällig" nur bis zur Cleanup-Grenze (nach 2 Monaten verschwinden die User systemisch) — mit Hinweis darauf. → F8 (nur VIP oder jeder Berater fürs eigene Team?).
|
||
|
||
---
|
||
|
||
## Offene Fragen an die Kundin (vor bzw. während der Umsetzung)
|
||
|
||
| # | Frage | Empfehlung |
|
||
|---|---|---|
|
||
| F1 | N1: „Teampartner insgesamt" — nur aktive Berater (wie die „Berater"-Spalte der Statistik) oder alle inkl. inaktiver? | aktive |
|
||
nur Aktive
|
||
| F2 | N1/N4: „Aktuelles Level" — die monatliche Qualifikationsstufe (bestimmt Payline/Provision, dazu passt „es fehlen X Punkte") oder das Karriere-Level (steht bereits in der Mitgliedschafts-Card)? | Quali-Stufe des Monats, mit Karriere-Level als Zusatzinfo |
|
||
JA
|
||
| F3 | N2: Home zeigt künftig Monats-Bestand (Rückrechnung; vor 07/2026 Näherung) — VIP-Statistik bleibt wie 07/2026 entschieden tagesaktuell? | ja, so lassen; sonst in einem Zug mit umstellen |
|
||
JA
|
||
| F4 | N3: Neupartner-Definition fürs Ranking — wie die Statistik-KPI (`aktiv geworden im Monat`) oder wie das Incentive (bezahltes Starterpaket im Monat)? (b) Neupartner-Drilldown: Spalte „Kundenpunkte" ersetzen durch „Punkte gesamt" oder beide zeigen? | (a) wie Statistik-KPI, damit Zahlen zusammenpassen; (b) beide |
|
||
a Wie KPI
|
||
| F5 | N4: Provisions-Prognose ist eine gekennzeichnete Schätzung (Rangverschiebungen/Boni nicht exakt) — reicht das? | ja, mit „unverbindlich"-Kennzeichnung |
|
||
Ja mit Kennzeichnung
|
||
| F6 | N5: Freitext-Herkunft als Pflichtfeld? | ja, Pflicht mit Beispiel-Platzhalter |
|
||
Ja
|
||
| F7 | N6: (a) Top 3 teamintern oder firmenintern? (b) „meiste neue Abos" = neue Kundenabos (+ Eigenabo) je Berater? (c) Foto-Einwilligungstext | (a) firmenintern hinter Schalter, (b) neue Kundenabos + Eigenabo, (c) Einwilligung beim Upload |
|
||
a teamintern
|
||
b neue Kundenabos + Eigenabo
|
||
c Einwilligung beim Upload - lagern wird aber nach
|
||
| F8 | N8: Jahresgebühr-Liste nur VIP-Statistik oder für jeden Berater (eigenes Team)? | zunächst VIP |
|
||
eigenes team
|
||
|
||
|
||
## Kontroll-Review (2026-08-19, /code-review high mit Fix-Anwendung)
|
||
|
||
Nach der Umsetzung lief eine mehrstufige Review (8 Finder-Perspektiven) über den gesamten Diff. **Angewendete Fixes:**
|
||
|
||
**Korrektheit:**
|
||
- **Level-Kachel/Prognose zeigten für Normalfälle „Höchstes Level erreicht"**: `qual_user_level_next` ist im Steady-State (Quali == Karriere-Level) bewusst null — `LiveCommissionService::resolveNextLevel()` fällt jetzt auf `next_qual_user_level`/`next_can_user_level` (direkt nächster Karriere-Level, inkl. `_calculated_*`) zurück. Verifiziert an Steady-State-User (Berater → Aktiv Berater).
|
||
- **„fehlen X Punkte" bei mehr Paylines im nächsten Level überhöht**: Basis ist jetzt `_calculated_payline_points_qual_kp` des NÄCHSTEN Levels (rechnet mit dessen Paylines/KP-Überschuss).
|
||
- **Historischer Abo-Bestand (N2) konnte deaktivierte Abos „wiederbeleben"**, wenn ein späteres Ereignis (z. B. Löschung) nach dem Stichtag lag: die Rückrechnung prüft jetzt zusätzlich, ob das letzte Statusereignis bis zum Stichtag deaktivierend war (inkl. Reaktivierungs-Logik). Test erweitert.
|
||
- **Top-3-Karte schrumpfte**, wenn ein Top-Kandidat keinen Account-Datensatz hat: Nachrücker füllen jetzt auf.
|
||
- **Payline-Abo-Kachel widersprach beim Monatswechsel den N2-Kacheln** (live vs. Monatsbestand): zählt jetzt ebenfalls monatsbezogen über `aboStockCount()`.
|
||
- Checkout-Pflichtfeld-Meldung (N5) war hardcodiert deutsch → Sprachkey `order.source_comment_required` (de/en/es/fr).
|
||
|
||
**Performance:**
|
||
- `lineBuckets()`-Baum-Traversierung lief pro Dashboard-Request 3–5×: jetzt Singleton + Prozess-Memo (`flushLineBucketsMemo()` für Batch/Struktur-Änderungen; `storeSnapshot()` flusht selbst).
|
||
- `levelAndCommissionForecast()` war als einzige neue Kennzahl ungecacht → 6-h-Cache (`level_forecast_*`).
|
||
- Snapshot-Cache-Key auf `live_business_v3_*` (Payload-Format geändert).
|
||
|
||
**Wiederverwendung/Konventionen:**
|
||
- Neue User-Scopes `firstlinesOf()` und `newPartnerIn()` — EINE Definition für Firstlines (Kachel, Seite, Top 3) und Neupartner (KPI, Recruiter-Ranking, Top 3) statt 3–4 Kopien.
|
||
- `SalesVolumePoints::totalPointsByUser()` als gemeinsamer Batch-Helper (Firstlines-Seite, Recruiter-Volumen, Top-3-Volumen).
|
||
- Slim-Cropper-Speicherlogik nach `Slim::storeCroppedImage()` extrahiert (Profilfoto + beide Shop-Uploads nutzen sie).
|
||
- `activeAbo`-Scope statt handgeschriebener Prädikate (Payline-Zähler, Top 3, N2-Rückrechnung); `formatDate()`/`getFullName(false)` statt Inline-Kopien; Firstlines-Filter auf `Form::open` (Projektregel); Return-Types ergänzt.
|
||
- Neue Tests: `TeamFirstlinesPageTest`, `ProfileImageTest` (die einzigen bis dahin ungetesteten neuen Endpunkte).
|
||
|
||
**Nachtrag (gleicher Tag, vom Kunden freigegeben) — die drei gemeldeten Punkte sind umgesetzt:**
|
||
1. ✅ **VP-Nachtreparatur gehärtet:** Der Live-Pfad (`Payment::addSponsorBuyingPoints` → `SalesPointsVolume::addSalesPointsVolume`) schreibt jetzt die `shopping_order_id` mit; der Repair dedupliziert primär darüber (immun gegen Umbenennungen), Alt-Zeilen ohne Referenz werden **zählend** je info-Text verbraucht (zwei gleiche Bestellungen ≠ eine). Scan-Fenster umfasst den **Vormonat** (Monatswechsel-Fälle heilen automatisch, Buchung weiterhin nur in den Zielmonat). Dedup ist **sponsor-unabhängig** mit zwei Fällen (Kundenentscheid 2026-08-19): Liegt die Buchung beim **Firmenaccount** (`config('mivita.company_user_id')`, User 6 — Registrierung über die Hauptwebseite ist ein User-Fehler, Punkte stehen immer dem Berater zu), wird **automatisch umgebucht** — im Zielmonat in place (eine Zeile, Syslog-Spur), aus einem abgeschlossenen Monat per neuer Buchung beim Berater + Gegenbuchung („VP-Storno") beim Firmenaccount im Zielmonat (der alte Monat bleibt unangetastet). Liegt sie bei einem anderen **Berater** (echte Umhängung), wird weiterhin nur gewarnt (`skipped_other_sponsor` + Command-Tabelle + Log). Am Echtfall Anna Schall (Order 58797) auf dem Testsystem ausgeführt: Juli-Zeile bei #6 unverändert, Christina Schuder (#1241) +200 P. im August mit Order-Referenz, Firmenaccount −200 P. im August; Folgelauf idempotent. Käufer/Sponsoren und Bestandszeilen werden gebatcht geladen (N+1 weg). 5 neue Tests, 12/12 grün.
|
||
2. ✅ **Incentive-Reparatur gehärtet:** `removeStaleSponsorTrackings()` überspringt `m_sponsor = NULL` (nur echter Sponsorwechsel löscht, mit Log-Spur); `repairForeignKeys()` läuft jetzt VOR der Stale-Bereinigung (Waisen-Logs ohne FK werden erst angebunden und dann konsistent mitbereinigt — vorher Doppelzählung möglich); N+1-Queries in Stale-Check und `rebuildFromSourceTables()` durch gebatchte Lookups ersetzt; Qualifikationsfenster zentral über neue Helper `Incentive::qualificationWindow()`/`qualificationWindowContains()` + Scope (vorher 4 unabhängige Kopien — die Fehlerklasse des Dino-Portaro-Randtag-Bugs). Kleine dokumentierte Abweichung: `registered_at` beim Rebuild = `MIN(created_at)` der Starterkit-Orders (statt „erste nach id"). 6 neue Tests (u. a. NULL-Sponsor bleibt erhalten, Randtag 23:15 zählt, End-to-End ohne Doppelzählung nach Umhängung).
|
||
3. ✅ **`.env` aus dem Git-Tracking entfernt** (`git rm --cached`; `.gitignore` enthielt sie bereits, `.env.example` existiert). Die Datei bleibt lokal/auf dem Server liegen — Pflege wie bisher per SFTP (vom Kunden bestätigt). Empfehlung bleibt: die bereits historisierten Keys (PAYONE, Stripe-Test, DHL) bei Gelegenheit rotieren.
|
||
|
||
## Technische Querschnittsthemen
|
||
|
||
- **`LiveCommissionService::buildSnapshot()` erweitern** (Level-/Punktefelder durchreichen) ist gemeinsame Voraussetzung für N1 und N4 — einmal bauen, zweimal nutzen. Cache-Key-Version erhöhen, damit alte 6-h-Caches nicht mit schmalem Payload kollidieren.
|
||
- **Performance Home-Dashboard:** alle neuen Kennzahlen in die bestehenden 6-h-Caches integrieren (`home_payline_*`-Muster); keine zusätzlichen Baum-Läufe.
|
||
- **Tests:** je Paket Pest-Feature-Tests analog der Juli-Suite (`BackofficeStatisticsListsTest`-Muster, Helfer mit eindeutigem Prefix, keine `route()`-Nutzung).
|
||
- **Unkommittetes WIP beachten:** `SponsorBuyingPointsRepairService` + `RepairSponsorBuyingPoints` (Nacht-Repair der Sponsor-VP-Zeilen) liegen ungetrackt im Arbeitsverzeichnis und berühren dieselbe Status-5-Logik wie N3 — vor Beginn von N3 committen/abstimmen.
|