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>
This commit is contained in:
Kevin 2026-09-23 15:45:10 +00:00
parent d6d8b3d716
commit cb4eaff95d
12 changed files with 2113 additions and 4 deletions

View file

@ -0,0 +1,92 @@
Ticket #1
Wir brauchen einen neuen Navigationspunkt, indem wir Berater übertragen können, d.h. die Kunden und den ganzen Berater Stamm auf einen einzelnen Berater.
Hintergrund ist, wenn zum Beispiel ein Berater aufhört, soll es die Möglichkeit geben, dass ein anderer Berater sein ganzen Berater und Kundenstamm bekommt ähnlich ist es wenn ein Berater nicht mehr zahlt, geht es an seinen Sponsor über.
Beispiel: Nadine Ried: Der ganze Baum soll an Claudia Zürn übertragen werden.
Ticket #2
Kunden löschen. Wenn ein Kunde eine Bestellung hat, kann man ihn so nicht mehr aus dem System löschen. Das ist etwas unglücklich unten gibt es einen Button DSGVO löschen, Hier werden die Sachen unkenntlich gemacht, das ist auch noch etwas unglücklich.
Ich würde es so machen kriegt man auf Kunden löschen geht ein Modal Fenster auf wo man die Möglichkeit hat den Kunden komplett zu löschen falls er noch keine Bestellung hat oder automatisch dort steht. Der Kunde hat schon eine Bestellung es geht nur DSGVO löschen. Wenn dann ein Kunde gelöscht wird oder entfernt wird, ist es wichtig, dass seine personenbezogenen Daten raus sind und auch weitere Kundenanlagen denn es ist möglich, dass der Kunde mehrfach angelegt ist. Wenn er mehrere Bestellung hat, wird jeweils ein Kunde angelegt d.h. hier müsste schon im Vorfeld geprüft werden. Wie oft der Kunde würde ich über die E-Mail-Adresse machen dies Unique in diesem Augenblick auswerten und sagen hier und hier wird der Eintrag entsprechend entfernt.
Ein Beispiel ist der Kunde 61009 Hier gibt es mehr Anlagen.
---
# Umsetzung (14.09.2026)
## Ticket #1 – Berater übertragen
**Neuer Navigationspunkt** im Superadmin-Bereich: *Berater übertragen* (unterhalb von *User Bereinigung*), URL `/admin/user/transfer`.
**Ablauf auf der Seite**
1. *Von Berater* (gibt den Stamm ab, alle nicht gelöschten Berater wählbar) und *An Berater* (übernimmt; nur aktive, nicht gesperrte Berater mit gültigem Account) über Live-Suche auswählen → *Vorschau anzeigen*.
2. Vorschau zeigt: Firstline-Berater (mit Liste), gesamte Downline (alle Ebenen), deaktivierte Berater mit geparktem Sponsor-Bezug, Kunden-Datensätze (davon gelöscht), Kunden-Abos (davon aktiv), Portal-Logins.
3. Checkbox bestätigen → *Übertragung ausführen*. Erfolgsmeldung mit Anzahl der bewegten Firstline-Berater, Kunden und Abos.
**Was übertragen wird** (in einer DB-Transaktion, `App\Services\ConsultantTransferService`)
- Firstline: `users.m_sponsor` = Quelle → Ziel. Die Teilbäume darunter folgen automatisch (sie hängen an ihrer Firstline).
- Geparkte Sponsor-Bezüge deaktivierter Berater: `users.pre_sponsor` = Quelle → Ziel (Reaktivierung landet dann unter dem Zielberater).
- Kunden: `shopping_users.member_id` = Quelle → Ziel, auch soft-gelöschte Einträge.
- Kunden-Abos: `user_abos.member_id` = Quelle → Ziel (künftige Abo-Bestellungen werden dem Zielberater zugeordnet).
- Kunden-Portal-Logins: `customers.member_id` = Quelle → Ziel.
**Was bewusst nicht übertragen wird**
- Bestehende Bestellungen (`shopping_orders.member_id`), Umsätze und Provisionsdaten bleiben als Historie beim bisherigen Berater. Bereits berechnete Monats-Snapshots (`user_businesses`, `user_business_structures`) werden nicht rückwirkend geändert; der laufende Monat wird bei der nächsten Provisionsberechnung (`business:store-optimized`) auf Basis der neuen Struktur gerechnet.
- Der Quellberater selbst bleibt unverändert (Sponsor, Status). Deaktivieren/Löschen erfolgt wie bisher separat (User-Verwaltung / DSGVO).
**Schutzregeln**
- Quelle ≠ Ziel; Ziel muss aktiv, nicht gesperrt, nicht gelöscht und mit gültigem Account sein.
- Ziel darf nicht *unterhalb* der Firstline der Quelle liegen (würde einen Kreis im Baum erzeugen) → Fehlermeldung.
- Ziel *in* der Firstline der Quelle ist erlaubt: Der Zielberater behält seinen Sponsor und bekommt die übrigen Firstline-Berater als Downline (Hinweis in der Vorschau).
**Protokollierung**: wie bei der automatischen User-Bereinigung – `user_clean_up_logs` (je umgehängtem Berater) und `shopping_user_member_logs` (je Kunde), damit die Übertragung in *User Bereinigung → Downline-Übertragungen / Kunden-Übertragungen* sichtbar ist. Zusätzlich SysLog-Eintrag `consultant-transfer` und Log-Kanal `cleanup`.
**Beispiel aus dem Ticket** (Nadine Ried → Claudia Zürn): *Von Berater* = Nadine Ried, *An Berater* = Claudia Zürn, Vorschau prüfen, bestätigen.
**Dateien**
- neu: `app/Services/ConsultantTransferService.php`, `app/Exceptions/ConsultantTransferException.php`, `app/Http/Controllers/AdminUserTransferController.php`, `app/Http/Requests/UserTransfer/{Preview,Execute}ConsultantTransferRequest.php`, `resources/views/admin/user/transfer/index.blade.php`, `tests/Feature/ConsultantTransferTest.php`
- geändert: `routes/domains/crm.php` (3 Routen im Superadmin-Block), `resources/views/layouts/includes/layout-sidenav.blade.php`, `resources/lang/{de,en,es,fr}/navigation.php` (`user_transfer`), `resources/lang/{de,en,es,fr}/msg.php` (`transfer_*`)
## Ticket #2 – Kunden löschen
**Neues Verhalten auf der Kunden-Detailseite (Admin)**: Der Button *Kunden löschen* öffnet ein Modal statt direkt zu löschen. Der alte Direkt-Link (GET) ist entfernt, die Löschung läuft jetzt per POST mit Bestätigungs-Checkbox.
**Das Modal zeigt**
- wie oft der Kunde angelegt ist, ermittelt über **Kundennummer ODER E-Mail-Adresse** (Groß-/Kleinschreibung egal; Platzhalter-Adressen wie Faker-/bereits anonymisierte Mails werden nicht zur Gruppierung benutzt, sonst würden fremde Kunden zusammengezogen). Beispiel Kunde 61009: alle Einträge mit derselben Kundennummer plus alle Endkunden-Einträge mit derselben E-Mail, auch mit anderer Kundennummer.
- eine Tabelle aller betroffenen Einträge (ID, Kd-Nr., Name, Berater, Anzahl Bestellungen, angelegt am, ggf. „gelöscht“).
- **Fall A – keine Bestellung in keinem Eintrag**: Hinweis *„kann komplett gelöscht werden“* + Checkbox + Button *Kunde komplett löschen* (Admin-Recht genügt).
- **Fall B – mindestens ein Eintrag hat eine Bestellung**: Hinweis *„Der Kunde hat schon N Bestellung(en). Es ist nur DSGVO löschen möglich“* + Checkbox + Button *Kunde DSGVO-löschen* (wie bisher nur Superadmin; normale Admins sehen den Hinweis).
- Bei aktivem Abo wird jede Löschung blockiert (Abo zuerst kündigen), bei Berater-Datensätzen wird auf die User-Verwaltung verwiesen.
**Komplett löschen** (`GdprDeletionService::purgeShoppingUser`), nur ohne Bestellungen:
- alle Einträge der Gruppe werden vollständig anonymisiert (Name, Adressen, Telefon, E-Mail, Notizen, Newsletter) **und** soft-gelöscht → verschwinden aus allen Listen;
- inaktive Abos verlieren E-Mail, Kartendaten und Zahlungs-Tokens (Payone/Stripe);
- Portal-Logins (`customers`) werden gelöscht;
- Stripe-Customer-Objekte werden gelöscht; Löschnachweis im SysLog (`gdpr-purge-customer`) und Log-Kanal `cleanup`.
- Kein Hard-Delete der Zeilen: Fremdschlüssel (Abos, Checkout-Funnel, Logs) zeigen auf `shopping_users`, und der Lösch-Nachweis bleibt erhalten. Personenbezogene Daten sind vollständig entfernt.
**DSGVO löschen** (bestehend, erweitert): arbeitet jetzt ebenfalls auf der E-Mail-Gruppe (nicht mehr nur Kundennummer) und entfernt zusätzlich die Portal-Logins. Wie bisher bleiben bei Einträgen mit Bestellung Name und Rechnungsadresse (Aufbewahrungspflicht), alle Kontaktdaten werden entfernt.
**Entfernt**: die alte „Löschung“ (`CustomerRepository::deleteCustomer`), die nur den einen Datensatz mit „Deleted User“ überschrieb und ihn in der Liste stehen ließ.
**Nebenkorrektur**: Fehlermeldungen der DSGVO-Löschung wurden mit dem Session-Key `alert-danger` gesetzt, den kein Layout anzeigt – jetzt `alert-error` (wird als Growl-Meldung angezeigt).
**Offener Punkt / Hinweis**: DHL-Versanddatensätze (`dhl_package_shipments`) enthalten Empfängerdaten je Bestellung und werden – wie Bestellungen und Rechnungen – bewusst nicht angefasst (Beleg zur Bestellung). Falls gewünscht, könnte dort bei DSGVO-Löschung zusätzlich E-Mail/Telefon geleert werden.
**Dateien**
- geändert: `app/Services/GdprDeletionService.php` (`getCustomerRows`, `groupingEmail`, `analyzeShoppingUser`, `purgeShoppingUser`, `deletePortalCustomers`), `app/Http/Controllers/CustomerController.php` (`delete` → POST + Purge, `detail` liefert Analyse), `app/Repositories/CustomerRepository.php`, `routes/domains/crm.php` (`admin_customer_delete` GET → POST), `resources/views/admin/customer/detail.blade.php` (Modal), `resources/views/admin/customer/_detail.blade.php` (Button), `resources/lang/{de,en,es,fr}/msg.php` (`gdpr_purge_*`, `gdpr_error_customer_has_orders`, `delete_confirm_checkbox_missing`)
- neu: `tests/Feature/CustomerDeleteTest.php`
## Tests
```bash
php artisan test --compact tests/Feature/ConsultantTransferTest.php tests/Feature/CustomerDeleteTest.php tests/Feature/GdprDeletionTest.php
```
Hinweis für HTTP-Tests auf Admin-Routen: Die CRM-Routen werden host-abhängig geladen. Die beiden neuen Testdateien setzen deshalb in `beforeEach` `$_SERVER['HTTP_HOST']` auf den CRM-Host, starten die App neu, migrieren die In-Memory-DB und stellen den Host in `afterEach` wieder her (sonst booten nachfolgende Tests mit den CRM-Routen).
Stand der Gesamt-Suite am 14.09.2026: 92 Fehlschläge bestehen unabhängig von diesen Tickets bereits vorher (identische Liste mit und ohne die neuen Dateien); alle 22 neuen Tests sind grün.

View file

@ -1,6 +1,6 @@
# Entwicklungsplan – Protokoll Zoom-Meeting 08.09.2026
**Stand:** 18.09.2026 · **Status:** C1/C2 live seit 14.09., A4, A3 und A1+A2 umgesetzt (committet, Deploy offen); Abo-Paket damit bis auf A5 komplett. Audit vom 18.09. ([2026-09-18-audit-commits-16-09-2026.md](2026-09-18-audit-commits-16-09-2026.md)) geprüft, alle Befunde umgesetzt — Stellungnahme und Deploy-Folgen dort. **E1 (Recruiting-Challenge) umgesetzt 18.09., Deploy offen** — Konfiguration im Admin, Details unten.
**Stand:** 23.09.2026 · **Seit 18.09.:** B2 umgesetzt (Deploy offen), E1 Gleichstand + Zählung nach Zahlungsdatum, A07, Warnung für überfällige Abos (`abo:check-overdue`) — Details in den Abschnitten. Kundenübersicht B2: [2026-09-23-mitgliedschaft-ablauf-uebersicht.md](2026-09-23-mitgliedschaft-ablauf-uebersicht.md). · Vorher: **Status:** C1/C2 live seit 14.09., A4, A3 und A1+A2 umgesetzt (committet, Deploy offen); Abo-Paket damit bis auf A5 komplett. Audit vom 18.09. ([2026-09-18-audit-commits-16-09-2026.md](2026-09-18-audit-commits-16-09-2026.md)) geprüft, alle Befunde umgesetzt — Stellungnahme und Deploy-Folgen dort. **E1 (Recruiting-Challenge) umgesetzt 18.09., Deploy offen** — Konfiguration im Admin, Details unten.
**Quellen:** `2026 09 Protokoll September-komprimiert.pdf` (Protokoll), `2026-09-14.md` (Ticket-Entwurf Abos #1–#3)
**Verwandte Pläne:** `dev/2026-07-22/entwicklungsplan-2026-09-07.md` (K11–K14 behandeln dieselben Themen aus dem Kundenbrief; die dortigen Rückfragen F4–F7 sind weiterhin offen), `dev/2026-09-10-business-plan/` (Ledger-Plan, löst A07/K11 langfristig)
@ -295,7 +295,34 @@ Dazu gibt es einen dritten, abweichenden Pfad: setzt ein Admin das Datum in die
- **Admin-Restore nach dem harten Cut** (`user:restore`) holt heute nur das Team über die Logs zurück. Laut Kundin ist nach dem Cut eine Neuregistrierung mit Starterpaket der Regelfall; Restore bleibt Admin-Ausnahme und holt zurück, was die Logs noch hergeben – so dokumentieren.
- Nebenbefunde: Kernel referenziert `UserCleanup::class`, Klasse heißt `UserCleanUp` (funktioniert nur durch case-insensitives Autoload); `AdminUserCleanupController::restore()` ruft `user:restore` zweimal auf (Zeilen 319/325).
**Aufwand:** 3–4 PT. **Entschieden (14.09.):** Sponsor bekommt bei Stufe 1 die Kurzinfo ohne Listen, bei Stufe 2 die Listen-Mail; der Status-Umbruch beim Deploy ist akzeptiert. **Offen (R3):** 2 Tage Versatz auch für Karten-/PayPal-Zahler oder dort sofort?
**Aufwand:** 3–4 PT. **Entschieden (14.09.):** Sponsor bekommt bei Stufe 1 die Kurzinfo ohne Listen, bei Stufe 2 die Listen-Mail; der Status-Umbruch beim Deploy ist akzeptiert. **R3 beantwortet:** 2 Tage Versatz für alle Zahlungsarten.
#### Umsetzung B2 (23.09.2026) — Deploy offen
- **Ein Weg für alle drei Pfade:** [`ConsultantSuspensionService`](../../app/Services/ConsultantSuspensionService.php) mit `suspend()`, `reinstate()`, `terminate()`. `user:cleanup`, der Zahlungseingang (`Payment::paymentStatusPaidAction`) und das Ablaufdatum im Admin (`AdminUserController`, save-account) rufen nur noch diese Methoden.
- **Fristen** in `config/mivita.php`: `cleanup_suspend_days` = 2, `cleanup_delete_days` = 28 (Env `MIVITA_CLEANUP_SUSPEND_DAYS` / `MIVITA_CLEANUP_DELETE_DAYS`).
- **Stufe 1 (Ablauftag + 2 Kalendertage, Nachtlauf 03:30):** Team wie bisher über `user_clean_up_logs`, dazu jetzt Kunden (`shopping_users`, auch gelöschte), Kundenabos (`user_abos.member_id`) und Portal-Logins (`customers`). Jede Umhängung steht in der neuen Tabelle `consultant_suspension_transfers` (Migration `2026_09_23_140613`); Kunden zusätzlich wie bisher in `shopping_user_member_logs`, damit sie in der User-Bereinigung sichtbar bleiben. Kurzinfo an den Sponsor (`SponsorSuspensionNoticeMail`), keine Mail an das Firmenkonto.
- **Rückgabe:** Team über die bestehenden Logs, Kundendaten über `consultant_suspension_transfers` — nur Datensätze, die noch bei dem Sponsor liegen, der sie bekommen hat. Von Hand weiter übertragene Kunden bleiben, wo sie sind.
- **Stufe 2 (Ablauftag + 28 Kalendertage; Zahlung bis Tag 27 gibt alles zurück):** was noch am Berater hängt (auch Altfälle, die nach der alten 2-Wochen-Regel inaktiv sind und deren Kunden nie umgezogen wurden), geht endgültig an den Sponsor. Der Berater wird Kunde des Sponsors — ohne Duplikat, wenn dort schon ein Kundendatensatz mit seiner E-Mail oder seinem Login liegt. Listen-Mail (`SponsorTransferMail`, Vorname/Nachname/Telefon/E-Mail) an jeden Sponsor, der Kunden oder Team dieses Beraters hält; beim Sponsor des harten Cuts steht der Berater selbst mit auf der Liste.
- **Texte:** Mail 35 und Dashboard-Hinweis nennen jetzt das Datum der Pause und die Frist für die Rückgabe (`:suspend_date`, `:deadline`), de/en/es/fr. Neue Sprachdatei `suspension.php`.
- **`user:cleanup --dry-run`** listet beide Stufen mit Team, Kunden, Kundenabos, Portal-Logins und bereits geparkten Datensätzen, ohne etwas zu ändern oder zu senden.
- **Nebenbefunde behoben:** Kernel referenziert `UserCleanUp` (vorher `UserCleanup`), `AdminUserCleanupController::restore()` ruft `user:restore` nur noch einmal auf.
- **Testschema:** `shopping_users.auth_user_id` ist auf Produktion nullable, in der Ursprungsmigration NOT NULL — `setUserToClient()` scheiterte deshalb nur in der Testdatenbank. Migration `2026_09_23_141152` zieht das nach und ändert nur, wo die Spalte noch NOT NULL ist (auf Produktion nichts).
- **Tests:** [`ConsultantSuspensionTest`](../../tests/Feature/ConsultantSuspensionTest.php) (12 Tests): Tag +1 nichts, Tag +2 Stufe 1 komplett, Kurzinfo mit Frist ohne Listen, keine Mail an Firma, Trockenlauf, Rückgabe per Zahlung, manuell verschobener Kunde bleibt, harter Cut mit Mail-Inhalt, kein Duplikat, Altfall mit liegengebliebenen Kunden, Admin-Pfad, `user:restore` einmal.
**Messung an der Live-Kopie (23.09.):** Der erste Lauf würde 6 Berater pausieren (64 Kundendatensätze, 2 Teampartner) und 20 Berater endgültig entfernen (150 Kundendatensätze, 1 Portal-Login). Keine aktiven Kundenabos betroffen. Heute liegen bei 24 inaktiven Beratern 162 Kundendatensätze, deren Umsätze bei niemandem ankommen.
**Deploy:**
```bash
php artisan migrate # consultant_suspension_transfers (+ Schema-Angleichung, auf Produktion ohne Wirkung)
php artisan view:clear && php artisan config:clear
php artisan user:cleanup --dry-run # Liste an Alois, erst danach laufen lassen
```
Der reguläre Lauf um 03:30 übernimmt danach. Kein Snapshot-Neuaufbau.
**Offen / bewusst nicht gebaut:** eigenes Berater-Abo (`is_for = me`) eines pausierten Beraters läuft zu Partnerkonditionen weiter (heute kein Fall); Admin-Restore nach dem harten Cut holt wie bisher nur das Team über die Logs zurück.
---
@ -476,7 +503,8 @@ Das Incentive-Modul trägt jetzt optional eine Geldprämie; Montenegro bleibt un
**Offen / bewusst nicht gebaut**
- Bonushöhe Top 3, Beschreibungstexte, Bild (Dani/Alois).
- **Punktgleichstand** in den Top 3: Rangfolge ist dann bestätigte Teilnahme vor anonym, sonst Datenbankreihenfolge. Falls es auf die Boni ankommt, Regel festlegen (z. B. wer die Anzahl zuerst erreicht hat) – Umsetzung 0,5 PT.
- ~~**Punktgleichstand** in den Top 3~~ — **umgesetzt 23.09.** (Teilnahmebedingungen Punkt 6): bei gleichen Punkten bestätigte Teilnahme vor anonym, danach wer den Stand zuerst erreicht hat (Zeitpunkt des letzten zählenden Neupartners bzw. Neuabos aus `incentive_new_partners.registered_at` / `incentive_new_abos.activated_at`), zuletzt die Teilnehmer-ID. `IncentiveTracker::updateRanking()`, Tests in `IncentiveRankingTest`. An der Live-Kopie gegengerechnet: Montenegro tauscht nur Plätze ab 81, die 20 Gewinner bleiben gleich.
- **Neupartner zählen ab der Zahlung (23.09.):** wie in der Statistik (Kundenentscheid 16.09.) zählt `users.partner_paid_at`, nicht das Bestelldatum — im Live-Tracking, in der nächtlichen Reparatur und im Neuaufbau (`IncentiveTracker::newPartnerCountedAt()`). Wer am 29.09. bestellt und am 01.10. zahlt, zählt in der Challenge. Montenegro ist abgeschlossen und bleibt unberührt; im Admin auf „Beendet“ stellen, damit der Nachtlauf es nicht mehr anfasst.
- **Stornos/Rücktritte:** Ein einmal getrackter Neupartner bleibt gezählt, auch wenn die Registrierung später storniert wird (Verhalten wie Montenegro). Vor der Auszahlung die Liste gegen stornierte Registrierungsbestellungen prüfen oder Regel mit Dani/Alois klären.
- Gutschrift-Erzeugung aus dem Export (siehe oben).
- Firmenweite Rangliste (D2) unverändert offen; die Challenge-Rangliste ist wie Montenegro für alle aktiven Berater sichtbar.