# Analyse: Feedback „Statistiken kontrolliert" (2026-07-24) Basis: `dev/2026-07-22/Statistiken kontrolliert.pdf` (Kundin, getestet mit User **454** / info@claudialattner.com). Alle Zahlen wurden am **2026-07-24** gegen die aktuelle Test-DB verifiziert (Datenstand von heute). > **Umsetzungsstand (2026-07-24): ✅ ALLE Punkte sind gemäß den „++"-Antworten des Kunden umgesetzt** — > Details je Punkt im jeweiligen Abschnitt „Umgesetzt". Tests: `BackofficeStatisticsListsTest` (12), > `TeamAboOverviewTest` (11), `PaylineForecastServiceTest` (6), `AboAdminDeleteTest` (3), > `Incentive/IncentiveRankingTest` (4, neu), `TeamStructureAboBadgesTest` (5, neu), > `YardShippingTaxTest` (neu) + erweiterte `AboOneTimeViewTest` — alle grün. > Einziger roter Test im Umfeld: `IncentiveTeaserPageTest` (Galerie-Bild `nikki-beach.jpg` liegt lokal im > Ordner) — vorbestehend, ohne Bezug zu diesen Änderungen. **Gesamtbild:** Von den 10 Punkten sind **2 echte Fixes** (Rang-Nummerierung Incentive, Porto-Anzeige), **3 kleine Erweiterungen** („ich" in Listen, Abo-Zähler Payline, Struktur-Symbole), **2 Erklärungs-/Beschriftungsthemen** (Zahlen stimmen, sind aber unterschiedliche Metriken) und **1 Konzeptänderung** (Führungskräfte: komplette Downline statt Abschneiden — widerspricht der früheren Entscheidung F9). Alle vom Kunden gemeldeten „falschen" Zahlen ließen sich vollständig aufklären — **kein Rechenfehler in den neuen Statistiken**. | # | Punkt | Einstufung | Aufwand | |---|---|---|---| | 1 | „Neue Kundenabos": ich selbst fehlt | Erweiterung (bewusster Ist-Stand) | klein | | 2a | Top 10 Anzahl: ich selbst fehlt | Erweiterung | klein (mit 1) | | 2b | Top 10 Anzahl ≠ Montenegro-Zahlen | **kein Bug** — andere Metrik, erklärbar | Doku/UI-Text | | 3 | Top 10 Umsatz: Soll/Ist unklar, Abo-Anzahl fehlt | Erweiterung + Beschriftung | klein | | 4 | Führungskräfte-Zahlen „stimmen nicht" | **Konzeptänderung** (F9 wird revidiert) | klein–mittel + Rückfrage | | 5 | Statistik 97.453 ≠ Struktur 92.211 | **kein Bug** — Neupartner-Punkte sind nicht payline-wirksam | Doku/UI-Text | | 6 | Was bedeuten 42.628 Abo-Punkte? | Erklärung + sinnvolle Erweiterung (Ist je Ebene) | klein–mittel | | 7 | Payline: Anzahl Abos anzeigen | Erweiterung | klein | | 8 | Montenegro: Nummerierung falsch | **Fix** (Ranking-Sortierung) | klein, aber mit Nebenwirkungen prüfen | | 9 | Porto DE Abo 19,90 vs. Einzel 16,72 | **Anzeige-Fix** (brutto vs. netto) | klein | | 10 | Struktur: Abo-Symbole je Teampartner | neue Anforderung | mittel | --- ## 1. „Neue Kundenabos": eigene Person muss erscheinen **Feedback:** „Hier muss ICH auch in die Liste aufgenommen werden, sollte ich neue Kundenabos abgeschlossen haben." **Befund/Ursache:** Alle Team-Auswertungen der VIP-Statistik basieren auf `lineBuckets()` ([BackofficeDashboardService.php:215-240](app/Services/Backoffice/BackofficeDashboardService.php#L215-L240)). Dort steht der Root-User von Anfang an in `visitedUserIds` und wird per `whereNotIn('id', …)` in **jeder** Linie ausgeschlossen. Damit fehlt die eingeloggte Person in „Neue Kundenabos" (`newCustomerAboRanking()`, Z. 499-533), in beiden Top-10-Listen und in der Führungskräfte-Liste. Einzige bisherige Ausnahme ist `shop_1000` — dort wurde der Root-User auf Kundenwunsch (B4) explizit ergänzt (Z. 96-98, Muster „(ich)" existiert also schon). Hinweis: User 454 hat aktuell **0 neue Kundenabos im Juli** — sie würde in dieser Liste derzeit ohnehin nicht auftauchen. Die Anforderung ist trotzdem korrekt. **Empfehlung:** Root-User-ID in die Kandidatenmenge von `newCustomerAboRanking()` und `topCustomerAboRanking()` aufnehmen (Linie 0 / Markierung „(ich)", analog `shop_1000`). Kleiner, klar umrissener Eingriff + Test. ++ so umsetzen wie in der Empfehlung **Umgesetzt (2026-07-24):** `usersWithLine()` nimmt optional den Root-User als Linie 0 auf; `newCustomerAboRanking()` und `topCustomerAboRanking()` (beide Listen) zählen ihn jetzt mit. Neue Zeilen tragen `is_self`; die View zeigt ein „ich"-Badge und „Eigene Kundenabos" statt „Linie 0". Test: „nimmt den Statistik-Nutzer selbst in die Kundenabo-Ranglisten auf". ## 2. Top 10 Kundenabos (Anzahl) ### 2a. Eigene Person fehlt Gleiche Ursache und Empfehlung wie Punkt 1. Zum Vergleich: User 454 hat aktuell **7 aktive Kundenabos** — wäre also derzeit nicht in den Top 10 (Platz 10 = 10 Abos), muss aber grundsätzlich mitspielen. ++ so umsetzen wie in der Empfehlung **Umgesetzt (2026-07-24):** mit Punkt 1 erledigt (beide Top-10-Listen inkl. „ich"). ### 2b. „Zahlen stimmen nicht" (Rosmarie 14 statt 13, Tanja 16 statt 13, …) **Befund: Beide Zahlen sind korrekt — es sind zwei verschiedene Metriken.** Die Service-Methode liefert heute exakt die Screenshot-Werte (Tanja 13 usw.), und die Montenegro-Werte stimmen ebenfalls exakt mit der DB überein. Die Montenegro-Spalte „ABOS" zählt etwas anderes als die Top-10-Liste: | | Top 10 Kundenabos (Anzahl) | Montenegro „ABOS x/6" | |---|---|---| | Was | **aktueller Bestand** aktiver Kundenabos | **im Qualifikationszeitraum (1.4.–31.7.) aktivierte** Abos | | Eigenabo des Beraters | zählt nicht (`is_for='ot'`) | zählt mit (+1, inkl. Alt-Eigenabo) | | Inzwischen gelöschte/inaktive Abos | zählen nicht | bleiben gezählt (Tracking-Tabelle `incentive_new_abos`, einmal erreicht = bleibt) | | Vor dem Zeitraum angelegte Abos | zählen (solange aktiv) | zählen, wenn **Aktivierung/erste Zahlung** in den Zeitraum fällt | Konkret nachgerechnet (DB-Stand heute), Beispiel **Tanja Longerich** (Montenegro 17 vs. Top-10 13): - 13 = aktive, nicht gelöschte Kundenabos heute (Top-10-Wert ✓) - 17 = 13 + **1 Eigenabo** + **2 im Juli gelöschte** Kundenabos (soft-delete am 06.07./21.07., im Mai angelegt) + **1 inzwischen storniertes** Abo (Status 6, Januar-Abo, erste Ausführung im April) Analog: Rosa Maria Assenbaum 15 = 13 + 1 Eigenabo + 2 gelöschte (16.07./21.07.) − 1 vor dem Zeitraum aktiviertes; Monika Kunz 16 = 13 + 1 + 2; Melanie Hoffmann-Gebhard 15 = 13 + 1 + 1. Die Erwartung der Kundin („Tanja 16") ist die Montenegro-Kundenabo-Zahl (17 − 1 Eigenabo) — kein Bestand. Code: Top-10 [BackofficeDashboardService.php:764-776](app/Services/Backoffice/BackofficeDashboardService.php#L764-L776) (`active=1`, `status NOT IN (4,5,6)`, SoftDelete-Scope) vs. Incentive [IncentiveParticipant.php:300-354](app/Models/IncentiveParticipant.php#L300-L354) + [IncentiveTracker.php:85-135](app/Services/Incentive/IncentiveTracker.php#L85-L135). **Empfehlungen:** 1. **UI-Text schärfen:** Untertitel der Top-10-Karte z. B. „aktueller Bestand aktiver Kundenabos — weicht bewusst von der Incentive-Zählung ab (dort zählen alle im Aktionszeitraum gestarteten Abos dauerhaft)". Dazu eine kurze Erklärung an die Kundin (Text kann aus der Tabelle oben übernommen werden). 2. **Daten-Auffälligkeit klären (separat!):** Es gibt im Team von 454 **7 Kundenabos, die im Juli per Soft-Delete gelöscht wurden, aber weiter `active=1`/Status 2-3 tragen** (Löschung vermutlich über den Admin-Delete [AboController.php:170-178](app/Http/Controllers/Admin/AboController.php#L170-L178), der Status/Flags nicht anfasst). Diese Abos werden durch den SoftDelete-Scope nirgends mehr ausgeführt oder gezählt — der inkonsistente Zustand (aktiv + gelöscht) sollte aber geprüft werden: War die Löschung beabsichtigt? Sollte `destroy()` zusätzlich `active=0`/Status setzen? ++ Umsetzen wie in der Empfehlung zusätzlich ja es ist richtig die Abos mussten gelöscht werden, da diese neu angelegt worden sind. Hier gab's Zahlungsprobleme grundsätzlich auf active null setzen und auch gleich die Funktion reparieren. **Umgesetzt (2026-07-24):** - UI-Text: Untertitel der Karte lautet jetzt „aktueller Bestand aktiver Kundenabos · weicht bewusst von der Incentive-Zählung ab" mit Erklär-Tooltip. - Funktion repariert: `UserAbo` hat einen `deleting`-Hook — jeder Softdelete setzt jetzt automatisch `active = false` (gilt für Admin-`destroy()`, Portal-`deletePending()` und alle weiteren Model-Deletes). Test in `AboAdminDeleteTest` („setzt beim Softdelete auch active auf false"). - Datenbereinigung: Migration `2026_07_24_120000_deactivate_soft_deleted_user_abos` deaktiviert alle bestehenden gelöschten-aber-aktiven Abos (auf dem Testsystem ausgeführt: 7 Datensätze bereinigt; läuft in Produktion beim Deploy). ## 3. Top 10 Kundenabos (Umsatz): Soll/Ist unklar, Abo-Anzahl fehlt **Feedback:** Warum ist Soll mal höher, mal niedriger als Ist? Was heißt „pro Zyklus"? Gewünscht: bereits **angerechnete** Kundenabopunkte des Monats + Anzahl der Abos dahinter (z. B. „Tanja: 1123 Punkte mit 16 Kundenabos"). **Befund/Ursache:** - **Soll** = Punktebestand der *heute* aktiven Kundenabos (Produktpunkte × Menge, ohne gelöschte/stornierte) — „was die Abos pro Lieferzyklus bringen" ([BackofficeDashboardService.php:721-736](app/Services/Backoffice/BackofficeDashboardService.php#L721-L736)). - **Ist** = im gewählten Monat tatsächlich abgerechnete Kundenabo-Punkte aus `user_sales_volumes` ([Z. 744-756](app/Services/Backoffice/BackofficeDashboardService.php#L744-L756)). - **Ist > Soll** entsteht, wenn Abos nach ihrer Abrechnung gelöscht/storniert wurden (Beispiel Tanja: 2 im Juli gelöschte Abos wurden Anfang Juli noch abgerechnet → stecken im Ist 1.123, fehlen im Soll 992) oder Mengen/Artikel reduziert wurden. **Ist < Soll**, wenn Abos im Monat neu dazukamen und ihr Liefertermin noch aussteht. Beides ist korrekt, nur unglücklich erklärt. **Empfehlung (kleiner Umbau):** 1. **Ist als Hauptwert** (Sortierung nach Ist statt Soll — die Kundin denkt vom abgerechneten Monatsumsatz her), Soll als Zweitwert. 2. **Spalte „Abos"** ergänzen (Anzahl aus `customerAboCounts()`, ist bereits berechnet). 3. Beschriftung: „Soll" → z. B. „Punkte pro Lieferung (heutiger Bestand)", „Ist" → „im {Monat} abgerechnet". 4. Eigene Person aufnehmen (Punkt 1/2a). ++ umsetzen wie in der Empfehlung. **Umgesetzt (2026-07-24):** Sortierung nach Ist (`points_actual`), neue Spalte „Abos"; Beschriftungen: „Abgerechnet (Punkte)" (Hauptwert, fett) und „Bestand/Lieferung" (Zweitwert), Untertitel „sortiert nach abgerechneten Kundenabo-Punkten {Monat}" mit Erklär-Tooltip; „ich" inklusive. Test: „sortiert die Top-10-Umsatzliste nach abgerechneten Ist-Punkten und liefert die Abo-Anzahl mit". ## 4. Führungskräfte-Statistik: „Zahlen stimmen leider nicht" **Feedback:** Teamabos/Team-Kundenabos sollen die **komplette Downline** umfassen. Beispiele: Rosmarie tatsächlich 81 Teamabos + 200 Teamkundenabos (Liste: 35 + 54), Nina 21 + 52 (Liste: 4 + 12). **Befund: Kein Rechenfehler — die Liste tut exakt das, was in F9 entschieden wurde:** Der Teilbaum jeder Führungskraft wird an der **nächsten Führungskraft abgeschnitten** ([BackofficeDashboardService.php:627-659](app/Services/Backoffice/BackofficeDashboardService.php#L627-L659), `continue` bei Leader-Kind), damit nichts doppelt zählt und die Summen additiv sind (Kopfzeile „464" = exakte Gesamtsumme). Nachgerechnet ohne Abschneiden (DB heute): **Rosa 82 Teamabos + 201 Teamkundenabos** (Kundin: 81/200 — Stand ihres Tests), **Nina exakt 21 + 52** wie von der Kundin angegeben. Die Kundin hat also die komplette Downline gezählt — ihre Erwartung widerspricht der eigenen Entscheidung F9 („an der nächsten Führungskraft abschneiden", siehe `entwicklungskonzept.md`, Entscheidungstabelle). **Konsequenz der Umstellung, die die Kundin kennen sollte:** Zählt jede Führungskraft ihre komplette Downline, werden Abos **mehrfach** gezählt (Ninas 73 Abos stecken dann auch komplett in Rosas 283). Die Kopf-Summe „464" und die Spaltensummen sind dann **keine** echten Gesamtwerte mehr. **Empfehlung:** 1. Umstellung auf komplette Downline ist technisch klein (das `continue` in `teamIdsPerLeader()` entfernen bzw. Schalter einbauen); Performance unkritisch (eine gemeinsame Sponsor-Map, BFS in memory). 2. **Vorher kurz an die Kundin:** „Dann zählen Abos bei mehreren Führungskräften gleichzeitig — die Gesamtsumme oben rechnen wir dann weiterhin ohne Doppelzählung. Okay?" (Kopfzeile auf Basis der Gesamtmenge, Zeilen mit kompletter Downline.) 3. Optional beide Sichten: Spalten „Team (direkt)" und „Team (gesamt)" — dann bleibt die additive Sicht erhalten. ++ wir sitzen Option drei, um dass wir beides zeigen und kurz erklären. Das ist relativ komplex und vermutlich wohl nicht verständlich. Von daher würde ich erst mal beides zeigen und dann klären, was dein sinnvoller ist.. **Umgesetzt (2026-07-24, Option 3 — beide Sichten):** `leaderAboOverview()` liefert je Führungskraft jetzt zusätzlich `team_abo_count_total`/`team_customer_abo_count_total` (komplette Downline, ohne Abschneiden) neben den bisherigen „direkt"-Werten. Die Tabelle zeigt beide Spaltenpaare — „Teamabos/Team-Kundenabos (direkt)" und „(gesamt)" — mit Tooltips; die **Gesamt-Spalte** rechnet mit der kompletten Downline (entspricht der Zählung der Kundin: eigenes Abo + Kundenabos + Teamabos gesamt + Team-Kundenabos gesamt), sortiert danach. Die **Kopf-Summe** bleibt bewusst überschneidungsfrei („direkt"), eine Fußnote unter der Tabelle erklärt den Unterschied. Test: „liefert je Fuehrungskraft zusaetzlich die Teamzahlen der kompletten Downline". → Nach Livegang mit der Kundin klären, ob beide Sichten bleiben oder eine reicht. ## 5. Statistik 97.453 ≠ Struktur 92.211 („theoretisch gleich") **Befund: Beide Zahlen sind korrekt — die Differenz sind Punkte, die nicht payline-wirksam sind.** - Die Statistik-Übersicht summiert je Linie `own_points + external_points` aus `user_sales_volumes` ([BackofficeDashboardService.php:437-464](app/Services/Backoffice/BackofficeDashboardService.php#L437-L464)) — also **alle** Umsatzpunkte der Team-Mitglieder. - Die Struktur-/Provisionsansicht (PP-Tabelle) summiert je Ebene `month_TP_points + month_shop_points` ([TreeCalcBotOptimized.php:279-300](app/Services/BusinessPlan/TreeCalcBotOptimized.php#L279-L300), [UserSalesVolume::getPointsTPSum()](app/Models/UserSalesVolume.php)) — nur die **Teampunkte (TP)**, die in die Payline der Upline fließen. - Der Unterschied entsteht in [SalesPointsVolume.php:56-64](app/Services/BusinessPlan/SalesPointsVolume.php#L56-L64): Einträge mit `status_points = 2` zählen **nur als KP (Karrierepunkte des Beraters), nicht als TP**. Nachgerechnet für das Team von 454, Juli 2026 (heute): Statistik-Summe **97.769**, TP-Summe **92.569**, Differenz **5.200** = **19 Neupartner-Einschreibungen** (Status 5, je 200/400 Punkte = 5.000) + **1 Gutschrift** (200 Punkte), alle mit `status_points=2`. Die Screenshot-Werte (97.453/92.211, Differenz 5.242) sind derselbe Effekt zum Testzeitpunkt. **Empfehlung:** Kein Fix nötig. In der Statistik-Übersicht eine Fußnote/Tooltip ergänzen: „enthält auch Punkte, die nicht in die Payline fließen (z. B. Neupartner-Einschreibungspunkte)" — optional als eigene Aufschlüsselungszeile (Datenbasis vorhanden: `status_points=2`-Summe je Linie). ++ Umsetzung wie in der Empfehlung. **Umgesetzt (2026-07-24):** Neues SQL-Fragment `SalesVolumePoints::nonPaylinePoints()` (status_points = 2) + Kennzahl `non_payline_points` im Overview/Snapshot. Unter der Linien-Tabelle erscheint eine Fußnote: „In den Gesamtpunkten enthalten: X Punkte, die nicht in die Payline fließen (z. B. Neupartner-Einschreibungspunkte) …" (nur wenn > 0; alte Snapshots ohne den Wert zeigen nichts). Test: „summiert Punkte ohne Payline-Wirkung (status_points 2) als eigene Kennzahl". ## 6. Team-Abo-Seite: „Was bedeuten diese 42.628 Abo-Punkte?" **Befund:** Die Spalte „Punkte" in „Abos nach Ebenen" ist der **Soll-Bestand pro Lieferzyklus**: Summe `getTotalPoints()` (Produktpunkte × Menge) aller **aktiven** Abos (Eigen- + Kundenabos) je Ebene — über **alle 10 Ebenen** des Teams ([TeamAboOverviewService.php:282-326](app/Services/Abo/TeamAboOverviewService.php#L282-L326)). Es ist also **nicht** der abgerechnete Umsatz, sondern „was alle Team-Abos pro Zyklus bringen würden". Zur Einordnung der Karte darüber: „Abo-Umsatz in der Payline" (Soll 38.375 / Ist 37.912) ist dieselbe Soll-Logik, aber nur für die **Payline** (6 Ebenen), plus Ist = im Monat abgerechnete Abo-Punkte ([PaylineForecastService.php:75-86](app/Services/Abo/PaylineForecastService.php#L75-L86)). 42.628 > 38.375, weil das Team 10 Ebenen hat, die Payline nur 6. Der Wunsch „Zahlen, die zum Stichtag 25. im aktuellen Monat abgerechnet werden" ist auf dem Home-Dashboard durch die **Prognose-Kacheln 5./10./20./25.** (B10) bereits abgedeckt. **Empfehlung:** 1. Spaltenüberschrift präzisieren: „Punkte pro Lieferzyklus (Bestand)". 2. **Ist-Spalte je Ebene** ergänzen („im Juli abgerechnet") — SQL-Fragment `SalesVolumePoints::aboPoints()` existiert bereits, nur je Ebenen-User-Menge gruppieren. 3. Optional dritte Spalte „davon bis zum 25. erwartet" (Logik aus `forecastDays()` wiederverwendbar). ++ Umsetzung wie in einer Empfehlung alle drei Punkte entsprechend umsetzen. **Umgesetzt (2026-07-24):** Die Ebenen-Tabelle hat jetzt drei Punkte-Spalten: „Punkte pro Lieferung (Bestand)" (bisherige Spalte, präzisiert), „abgerechnet ({Monat})" (`abo_points_billed`, gleiche Zählweise wie der Payline-Ist-Wert) und „bis zum 25. erwartet" (`abo_points_expected` = abgerechnet + offene Ausführungen mit `next_date` bis zum 25.; nur im laufenden Monat, sonst = abgerechnet) — inkl. Summenzeile und Tooltip. Sprachkeys in de/en/es/fr. Test: „liefert je Ebene abgerechnete Abo-Punkte und die Prognose bis zum letzten Liefertag". ## 7. Payline: Anzahl der Abos ausweisen **Feedback:** „Wieviel ABOs gibt es in der Payline? z. B. 205 Teampartnerabos + 302 Teamkundenabos (6 Ebenen)." **Befund:** Die Karte zeigt bisher nur „Payline = 6 Ebenen · 504 Berater". Die Zähler sind trivial ermittelbar (gleiche Filter wie `aboTargetPoints()`, [PaylineForecastService.php:164-183](app/Services/Abo/PaylineForecastService.php#L164-L183)). Heute für User 454: 6 Ebenen, 502 Berater, **129 Teampartner-Abos + 340 Team-Kundenabos** (die Beispielzahlen der Kundin waren Schätzungen). **Empfehlung:** `aboTurnover()` um `team_abo_count`/`team_customer_abo_count` erweitern und in der Kartenunterzeile anzeigen: „6 Ebenen · 502 Berater · 129 Teamabos + 340 Kundenabos". Aufwand klein, Test in `PaylineForecastServiceTest` ergänzen. ++ Umsetzung wie in einer Empfehlung **Umgesetzt (2026-07-24):** `aboTurnover()` liefert zusätzlich `team_abo_count`/`team_customer_abo_count` (gleiche Filter wie das Soll); die Karten-Unterzeile zeigt jetzt z. B. „Payline = 6 Ebenen · 502 Berater — 129 Teamabos + 340 Kundenabos in der Payline" (Sprachkey `abo.payline_abo_counts`, de/en/es/fr). Test in `PaylineForecastServiceTest` erweitert. ## 8. Live-Rangliste Montenegro: Nummerierung **Feedback (berechtigt):** Gewinner müssen 1, 2, 3, … nummeriert sein; wer mehr Punkte hat, aber die Mindestqualifikation nicht erfüllt, wird **darunter** gereiht (Beispiel Tanja: meiste Punkte, nicht qualifiziert → Platz 5, nicht Platz 1). **Befund/Ursache:** Der gespeicherte Rang ist ein **reines Punkte-Ranking über alle Teilnehmer** ([IncentiveTracker.php:320-341](app/Services/Incentive/IncentiveTracker.php#L320-L341), Qualifikation spielt keine Rolle). Die **Anzeige** sortiert aber zuerst nach `is_qualified DESC` ([IncentiveParticipant.php:126-143](app/Models/IncentiveParticipant.php#L126-L143)). Ergebnis: Qualifizierte stehen oben, tragen aber ihre globalen Punkte-Ränge (4, 7, 9), darunter stehen Rang 1, 2, 5 — genau die gemeldete Verwirrung. **Zusätzliche Brisanz:** `isWinner` prüft `rank <= max_winners` ([IncentiveParticipant.php:519-526](app/Models/IncentiveParticipant.php#L519-L526)). Mit dem heutigen Punkte-Rang könnte ein qualifizierter Berater mit globalem Punkte-Rang 21+ **fälschlich kein Gewinner** sein, obwohl weniger als 20 qualifiziert sind. Die Sortier-Korrektur repariert also auch die Gewinner-Logik. **Empfehlung:** `updateRanking()` auf `ORDER BY is_qualified DESC, total_points DESC` umstellen (Tie-Break unverändert) — dann ist der gespeicherte Rang identisch mit der Anzeigereihenfolge: Qualifizierte zuerst (1..n), Nicht-Qualifizierte dahinter; „springt nach oben", sobald die Qualifikation erreicht ist. **Vorher prüfen:** Wo wird `rank` noch verwendet (Mails/Benachrichtigungen bei Rangänderung, `withRankingActivity()`)? Rangsprünge durch die Umstellung könnten einmalig Benachrichtigungen auslösen — ggf. Benachrichtigung für den Umstellungs-Lauf unterdrücken. Test ergänzen (qualifiziert schlägt Punkte). ++ Umsetzung wie in einer Empfehlung **Umgesetzt (2026-07-24):** `IncentiveTracker::updateRanking()` sortiert jetzt `is_qualified DESC → total_points DESC → Terms-Tie-Break`; der gespeicherte Rang entspricht damit exakt der Anzeigereihenfolge (Qualifizierte 1..n, danach die Nicht-Qualifizierten; wer sich qualifiziert, springt hoch — genau das Kundin-Beispiel „Tanja → Platz 5"). Nebenwirkungen geprüft: `is_qualified` wird vor jedem Ranking-Lauf frisch berechnet, es existiert **kein** Rang-Benachrichtigungsmechanismus (keine Mail-Flut möglich), und `isWinner (rank <= max_winners)` wird durch den Fix erst korrekt (vorher konnte ein Qualifizierter mit Punkte-Rang 21+ fälschlich leer ausgehen). Neuer Test `Incentive/IncentiveRankingTest` (4 Fälle); ein vorbestehend roter Unit-Test (Reflection auf falsche Klasse) wurde mitrepariert. ## 9. Porto Deutschland: Abo 19,90 vs. Einzelbestellung 16,72 **Befund: Es gibt keinen echten Preisunterschied — es ist derselbe Versandsatz, einmal brutto und einmal netto angezeigt.** 16,72 × 1,19 = 19,90. - Konfiguration: `shipping_prices.price_comp` (Teampartner-Versand, **brutto**): DE **19,90 €**, AT **30,00 €**, jeweils `tax_rate` 19 %. - Der **Abo-Checkout** zeigt für steuerpflichtige DE-Teampartner den **Brutto**-Versand: [_order_combined_summary.blade.php:31](resources/views/admin/abo/_order_combined_summary.blade.php#L31), [_order_abo_show.blade.php:93](resources/views/admin/abo/_order_abo_show.blade.php#L93) (`$taxFree ? shippingNet() : shipping()`), [portal/abo/_create_check.blade.php:158](resources/views/portal/abo/_create_check.blade.php#L158) sogar fest brutto. - Die **Einzelbestellungs-Übersicht** ist eine Netto-Aufstellung und zeigt **netto**: [api_order_list_total.blade.php:57](resources/views/user/shop/sales/api_order_list_total.blade.php#L57) → 19,90 / 1,19 = **16,72 €**. - **Österreich:** Teampartner mit gültiger USt-ID ist Reverse-Charge-steuerfrei (`user_tax_free`, [UserService.php:103-137](app/Services/UserService.php#L103-L137)) → auch der Abo-Zweig zeigt netto: 30,00 / 1,19 = **25,21 €** — deshalb dort „passt es". Die tatsächlich berechneten Gesamtbeträge sind konsistent (netto + MwSt); es ist eine **Anzeige-Inkonsistenz**, kein Abrechnungsfehler. **Empfehlung:** Porto-Ausweis vereinheitlichen — im Abo-Checkout in der Netto-Aufstellung den **Netto**-Versand zeigen (bzw. „19,90 € inkl. MwSt" kennzeichnen), damit Abo und Einzelbestellung vergleichbar sind. Nebenbefund: [Yard.php:323](app/Services/Yard.php#L323) berechnet `shipping_tax` falsch (liefert den Nettowert statt des Steueranteils) — mit fixen. ++ Umsetzung wie in einer Empfehlung, Mit einer Kennzeichnung, denn wichtig ist. Bei MU Preise müssen wir immer den Bruttowert anzeigen. Bei den Beratern im System können wir den Nettowert anzeigen. Das ist leider nun mal so, dass die Berater als selbstständige im netto handeln und alles, was nach außen geht, an Kunden brutto zu behandeln ist. Das müsste einmal geprüft werden, dass das einheitlich ist und dann bitte netto oder inklusive Mehrwertsteuer kennzeichnen. **Umgesetzt (2026-07-24) nach der Regel „Berater netto (gekennzeichnet), Endkunden brutto (gekennzeichnet)":** - Wichtiger Befund: `_order_combined_summary` und `_order_abo_show` werden **auch im Kundenportal** eingebunden. Deshalb neues View-Flag `portal_view` (Default = Berater), gesetzt von den Portal-Aufrufern (`Portal/AboController`, `portal/abo/my_abo`). - Berater-Ansichten (Abo-Detail Berater/Admin, Einzelbestellungs-Übersicht): Versand **netto** + Kennzeichnung „(netto)" (Key `order.net`, existierte). Die Netto-Summenzeilen passen jetzt rechnerisch zur Porto-Zeile. - Kundenportal (Abo anlegen/ansehen): Versand **brutto** + „(inkl. MwSt.)" (neuer Key `order.incl_VAT`, de/en/es/fr); bei Reverse-Charge/steuerfrei automatisch netto + „(netto)". - Nebenbefund gefixt: `Yard::shipping_tax` berechnete den Nettowert statt des Steueranteils (jetzt brutto − netto); alle anderen `shipping_tax`-Verwendungen (HomepartyCart, ShopApiOrderCart, Correction) waren bereits korrekt und blieben unverändert. Tests: `YardShippingTaxTest` (neu) + 2 Rendering-Tests in `AboOneTimeViewTest`. - **Nachtrag (gleicher Tag):** Drei weitere Versandkosten-Anzeigen ergänzt, die in der ersten Runde fehlten (dort waren die Kundin-Screenshots entstanden): CRM-Warenkorb der Berater [user/order/yard_view_form.blade.php](resources/views/user/order/yard_view_form.blade.php) → „(netto)"; Shop-Warenkorb [web/templates/card.blade.php](resources/views/web/templates/card.blade.php) und Shop-Checkout [web/templates/checkout.blade.php](resources/views/web/templates/checkout.blade.php) → „(inkl. MwSt.)", bei Reverse-Charge/steuerfrei netto + „(netto)" (dort zeigte die Porto-Zeile zuvor sogar brutto über einer Netto-Summenzeile). Nicht angefasst: Homeparty-/Sammelbestellungs- und PDF-/Mail-Ansichten (eigene Belegformate). - **Für die Kundin:** Es gab nie einen Preisunterschied — DE-Teampartner-Porto ist 19,90 € brutto = 16,72 € netto; jetzt ist überall gekennzeichnet, welcher Wert gezeigt wird. ## 10. Struktur-Ansicht: Symbole „A" (eigenes Abo) und „(n)" (Kundenabos) **Feedback (neue Anforderung):** In der Struktur je Teampartner zwei Kennzeichen: grünes **A** = hat eigenes Abo, rotes **(8)** = Anzahl eigener Kundenabos. **Einschätzung:** Machbar, Datenbasis vorhanden (aktives `me`-Abo je `user_id`, Kundenabo-Zählung je `member_id` — gleiche Queries wie in der Statistik). Umsetzungsort: Struktur-/Tree-Rendering ([TeamController.php](app/Http/Controllers/User/TeamController.php), `TreeHtmlRenderer`). Wichtig: Zählungen **gebatcht** für alle sichtbaren Knoten laden (zwei Group-by-Queries), nicht pro Knoten. Aufwand mittel (inkl. Badge-Styling + Test). Vorschlag: gleiche Zählweise wie Top-10 (aktiver Bestand, ohne gelöschte) — dann sind Struktur und Statistik konsistent. ++ So umsetzen **Umgesetzt (2026-07-24):** Badges im `TreeHtmlRenderer` (Struktur-Ansicht, `TeamController::structure()`): grünes „A"-Badge bei eigenem aktivem Abo, rotes „(n)"-Badge mit der Anzahl aktiver eigener Kundenabos (nur bei n > 0), direkt neben dem Karriere-Level. Zählweise identisch zur Statistik (`activeAbo`, ohne gelöschte); Daten werden für den ganzen Baum mit zwei gebatchten Queries geladen (keine Query pro Knoten). Tooltips über Sprachkeys `team.badge_own_abo`/`team.badge_customer_abos` (de/en/es/fr). Neuer Test `TeamStructureAboBadgesTest` (5 Fälle). --- ## Rückfragen an die Kundin — Stand nach Umsetzung 1. ~~Führungskräfte direkt vs. gesamt~~ → **entschieden: beide Sichten** (Option 3, umgesetzt). Nach Livegang klären, ob beide Spaltenpaare bleiben. 2. ~~Top-10-Umsatz-Sortierung~~ → **entschieden: nach Ist** (umgesetzt). 3. ~~Gelöschte aktive Abos~~ → **beantwortet:** Löschungen waren beabsichtigt (Abos wurden wegen Zahlungsproblemen neu angelegt). Fix: Softdelete setzt jetzt `active=0` (Model-Hook) + Datenbereinigung per Migration. 4. **Neu/offen (F12 aus dem ursprünglichen Konzept, weiterhin unbeantwortet):** Zählen eigene Abos/Kundenabos in der Payline-Prognose mit? Aktuell ja (`PaylineForecastService::INCLUDE_OWN_ABOS = true`). ## Anhang: Verifikationsdaten (DB-Stand 2026-07-24, Team von User 454) | Beraterin | Top-10 (aktiver Bestand) | davon-Rechnung Montenegro | |---|---|---| | Tanja Longerich (909) | 13 | 17 = 13 + 1 Eigenabo + 2 gelöschte + 1 storniertes | | Rosa M. Assenbaum (456) | 13 | 15 = 13 + 1 Eigenabo + 2 gelöschte − 1 vor Zeitraum aktiviert | | Monika Kunz (1218) | 13 | 16 = 13 + 1 Eigenabo + 2 gelöschte | | Melanie Hoffmann-G. (1143) | 13 | 15 = 13 + 1 Eigenabo + 1 gelöschtes | | Claudia Lattner (454) | 7 (fehlt in Liste, Punkt 1) | 8 = 7 + 1 Eigenabo | - Statistik-Linien-Summe heute: 97.769; TP-Summe (Struktur): 92.569; Differenz 5.200 = 19 Neupartner-Einschreibungen (5.000 P.) + 1 Gutschrift (200 P.), alle `status_points=2`. - Führungskräfte komplette Downline heute: Rosa 82 Teamabos / 201 Teamkundenabos (Kundin: 81/200), Nina 21 / 52 (exakt wie Kundin). - Payline 454 heute: 6 Ebenen, 502 Berater, 129 Teamabos, 340 Teamkundenabos. - Montenegro-Incentive: `incentives` id 2, Qualifikation 01.04.–31.07.2026, min. 4 Partner / 6 Abos, max. 20 Gewinner. ## Anhang: Geänderte Dateien (Umsetzung 2026-07-24) **Services/Models:** `BackofficeDashboardService` (ich-Aufnahme, Ist-Sortierung, direkt+gesamt-Teamzahlen, `nonPaylinePointsSum`), `SalesVolumePoints::nonPaylinePoints()`, `TeamAboOverviewService` (`abo_points_billed`/`abo_points_expected`), `PaylineForecastService` (Abo-Zähler), `UserAbo` (deleting-Hook), `IncentiveTracker::updateRanking()`, `Yard` (shipping_tax), `TreeHtmlRenderer` (Abo-Badges), `Portal/AboController` (`portal_view`). **Views:** `user/backoffice/statistics/index.blade.php`, `user/team/abos.blade.php`, `admin/abo/_order_combined_summary.blade.php`, `admin/abo/_order_abo_show.blade.php`, `portal/abo/_create_check.blade.php`, `portal/abo/my_abo.blade.php`, `user/shop/sales/api_order_list_total.blade.php`. **Sprachdateien:** `abo.php` (payline_abo_counts, abo_points_per_cycle/billed/expected + hint), `order.php` (incl_VAT), `team.php` (badge_own_abo, badge_customer_abos) — jeweils de/en/es/fr. **Migration:** `2026_07_24_120000_deactivate_soft_deleted_user_abos` (Datenbereinigung gelöschte-aber-aktive Abos). **Tests:** neu `Incentive/IncentiveRankingTest`, `TeamStructureAboBadgesTest`, `YardShippingTaxTest`; erweitert `BackofficeStatisticsListsTest`, `TeamAboOverviewTest`, `PaylineForecastServiceTest`, `AboAdminDeleteTest`, `AboOneTimeViewTest`, `Unit/Incentive/IncentiveTrackerTest`.