Audit 18.09.: Zahlungsdatum nur bei Zahlung, Neupartner-IDs im Snapshot, Pausenende folgt dem Termin

F1: partner_paid_at wird beim Rechnungskauf (paymentStatusPaidAction mit
paid=false) nicht mehr gesetzt, sondern erst bei der Rechnungszahlung; der
Backfill nimmt das Aktivierungsdatum nur noch ohne jede
Registrierungsbestellung (offene Rechnung bleibt leer).
F2: Snapshots speichern je Linie new_partner_ids, die Neupartner-Liste zeigt
genau diese; aeltere Snapshots ohne IDs rechnen mit der damaligen Regel
(Registrierungsmonat, scopeNewPartnerInByActiveDate), damit Kachel und Liste
uebereinstimmen. Keine Payload-Versionsaenderung, kein Neuaufbau noetig.
F3: Liefertag-/Termin-Aenderung waehrend einer Pause zieht paused_until mit;
eine Ausfuehrung loescht auch den paused_by_*-Grund.
F4: expiredCount der Firstlines-Seite (Controller, View, Sprachen) zum bereits
committeten Test nachgezogen.
F5: Backfills schreiben ohne updated_at (toBase()->update, timestamps=false).
Kuendigungsbestaetigung abo_cancel_confirm wird im Selfservice serverseitig
verlangt. AboStatusExplainer laedt die Historie je Abo und Aktion nur einmal.

Stellungnahme und Deploy-Folgen im Audit-Dokument, Regressionstests je Befund.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin 2026-09-18 08:59:45 +00:00
parent b7e8bbd951
commit 03416b0afe
27 changed files with 560 additions and 21 deletions

View file

@ -0,0 +1,223 @@
# Audit der vier Commits vom 16.09.2026
**Prüfdatum:** 18.09.2026
**Geprüfter Stand:** `d5ad1ba50b8b4442d6b6ccffebb0226f90c04894`
**Vergleichsbasis:** `203999a` (`HEAD~4` zum Prüfzeitpunkt)
**Umfang:** Code, Ticketanforderungen, Migrationen/Backfills, Aufrufer, Views, Übersetzungen und automatisierte Tests.
## Bewertung
Die Umsetzung hat eine gute Grundlage: Datumsberechnung und Sperrregeln sind zentralisiert, Monatsenden werden berücksichtigt, und die Statusanzeige verwendet einen gemeinsamen Explainer. Die neuen Tests decken zahlreiche reguläre Abläufe ab. **Den Gesamtstand würde ich wegen des falsch gesetzten Zahlungsdatums und der inkonsistenten historischen Statistik noch nicht als abgeschlossen abnehmen.**
Es gibt fünf konkrete Befunde: einen mit hoher, drei mit mittlerer und einen mit niedriger Priorität. Hinzu kommen gezielte Verbesserungen bei Bestätigung, Datenqualität, Performance und Testabdeckung. Die vorhandene Testsuite ist schon vor diesen Änderungen nicht vollständig grün; durch die vier Commits kommt genau ein weiterer fehlgeschlagener Test hinzu.
| Commit | Umsetzung | Einschätzung |
| --- | --- | --- |
| `c0535be` | A4: sichtbarer Bereich für Pause/Kündigung, Folgen-Hinweis und Bestätigungs-Checkbox | UI sinnvoll ergänzt; Mindestlaufzeit und Sperre bleiben serverseitig abgesichert. Checkbox selbst wird nur im Browser geprüft. |
| `ec3ecad` | A3: Statusgrund, Pausenende, Zahlungsfehler und Backfill | Gemeinsame Darstellung ist sinnvoll; Pausenende kann nach Terminänderungen falsch sein, Backfill verändert entgegen seiner Beschreibung Zeitstempel. |
| `66c6ae0` | A1/A2: Liefertage 1–31, Monatsende, Sperre am Ausführungstag | Datums- und Kalendertagslogik im geprüften Umfang schlüssig; Zusammenspiel mit der neuen Pauseninformation muss ergänzt werden. |
| `d5ad1ba` | Neupartner im Zahlungsmonat | Zentrale Definition sinnvoll, aber Rechnungskauf und bestehende Snapshots sind unvollständig berücksichtigt; ein Test wurde ohne die zugehörige Controller-Änderung committet. |
## Prüfverfahren und Grenzen
Im Arbeitsverzeichnis lagen bereits umfangreiche uncommittete Änderungen. Deshalb wurden beide Git-Stände mit `git archive` in separate temporäre Verzeichnisse extrahiert und mit Kopien der vorhandenen PHP-Abhängigkeiten getestet. Die lokale Anwendungs-Konfiguration mit echten Zugangsdaten wurde nicht übernommen; verwendet wurden SQLite im Speicher und Testkonfiguration. Die bereits vorhandenen Änderungen wurden nicht bearbeitet.
Ticketgrundlage waren insbesondere [Entwicklungsplan September](entwicklungsplan-protokoll-september-2026.md), Abschnitte A1–A4 und Antworten R1/R2, sowie [Neupartner im Zahlungsmonat](../2026-07-22/neupartner-zahlungsmonat.md). Fundstellen und Zeilennummern beziehen sich auf den geprüften Commitstand; uncommittete Änderungen können die Zeilennummern im Arbeitsverzeichnis verschieben.
Die Prüfung umfasst keine Browserinteraktion, keinen produktiven Datenabgleich und keinen MySQL-Parallelitätstest. Es wurden keine fachlichen Backfills oder Migrationen gegen die bestehende Anwendungsdatenbank ausgeführt. Im Repository wurde ausschließlich dieser Bericht neu angelegt; zusätzliche Reproduktionstests liegen nur in der temporären Prüfkopie.
## Konkrete Befunde
### F1 – Hohe Priorität: Unbezahlte Rechnung setzt bereits das erste Zahlungsdatum
**Commit:** `d5ad1ba`
**Fundstellen:** [Payment.php](../../app/Services/Payment.php), Zeilen 224–230; [UserUtil.php](../../app/Services/UserUtil.php), Zeilen 285–293. Relevanter Aufrufer: [CheckoutController.php](../../app/Http/Controllers/Web/CheckoutController.php), `directPaymentStatus()`, im Commitstand Zeilen 760–773.
`Payment::paymentStatusPaidAction()` wird beim Rechnungskauf ausdrücklich mit `$paid = false` aufgerufen. Der neue Aufruf von `markPartnerPaid()` prüft dieses Argument nicht. Dadurch wird schon bei einer offenen Rechnung ein Zahlungszeitpunkt gespeichert. Bei der späteren tatsächlichen Zahlung bleibt dieser Wert wegen des gewollten „nur einmal setzen“-Guards bestehen.
**Reproduziert:** Konto-Produkt auf Rechnung am 31.08.2026 → Bestellung bleibt `paid = false`, aber `partner_paid_at = 31.08.2026`. Zahlung am 01.09.2026 → `partner_paid_at` bleibt August; `isNewPartnerIn(9, 2026)` liefert `false`.
**Auswirkung:** Die zentrale Ticketanforderung „Monat der Zahlung“ ist für diesen Pfad nicht erfüllt. Kachel, Recruiter-Auswertung und Neupartner-Liste können den falschen Monat verwenden. Auch nie bezahlte Rechnungskäufe erhalten bereits einen Zahlungszeitpunkt.
**Empfehlung:** Im Zahlungsservice das Feld nur bei bestätigter Zahlung setzen. Die ausdrücklich erlaubte manuelle Freischaltung im Admin bleibt ein eigener Fall. Zusätzlich im Backfill unterscheiden, ob ein Konto tatsächlich manuell freigeschaltet wurde oder lediglich eine offene Rechnung vorliegt: `payment_account != null` und fehlende bezahlte Registrierungsbestellung beweisen keine Admin-Freischaltung. Bereits falsch gesetzte Werte müssen bei einer Korrektur gezielt identifiziert werden; ein normaler erneuter Zahlungslauf repariert sie nicht.
**Fehlender Regressionstest:** Offene Rechnung Ende August, Zahlung Anfang September; vor Zahlung kein `partner_paid_at`, danach September. Außerdem offener Rechnungskauf ohne Zahlung im Backfill.
### F2 – Mittlere Priorität: Historische Kachel und verlinkte Neupartner-Liste verwenden unterschiedliche Regeln
**Commit:** `d5ad1ba`
**Fundstellen:** [User.php](../../app/User.php), `scopeNewPartnerIn()`, Zeilen 264–274; [BackofficeDrilldownService.php](../../app/Services/Backoffice/BackofficeDrilldownService.php), Zeilen 36–52 und 152–159.
Abgeschlossene Monate zeigen weiterhin die gespeicherten Snapshot-Zahlen nach der bisherigen Definition. Die Detailansicht übernimmt aus dem Snapshot nur die Teammitglieder und filtert diese anschließend mit dem geänderten `newPartnerIn()` nach `partner_paid_at`. Die eingefrorene Neupartner-Mitgliedschaft wird nicht übernommen.
**Reproduziert:** Ein August-Snapshot enthält einen Neupartner nach Registrierungsdatum. Nach Hinterlegung des tatsächlichen Zahlungsdatums im September zeigt die August-Kachel weiterhin **1**, die verlinkte August-Liste aber **0** Zeilen. Der CSV-Export verwendet ebenfalls diese Detaildaten.
Die Deploy-Dokumentation beschreibt bereits, dass alte Snapshots bewusst nicht automatisch neu aufgebaut werden. Das erklärt die unveränderte Kachel, löst aber nicht die neu entstehende Abweichung innerhalb derselben Ansicht. Auch die dokumentierten wiederhergestellten Partner mit deutlich früherer Erstzahlung können hiervon betroffen sein; die produktive Anzahl wurde im Audit nicht ermittelt.
**Empfehlung:** Snapshot und Drilldown müssen dieselbe Definitionsversion verwenden. Für künftige Snapshots auch die Neupartner-IDs speichern. Für bestehende Snapshots eine explizite Strategie vorsehen: alte Definition konsistent weiter anzeigen oder gezielt abgestimmt migrieren. Ein pauschales `--force` ist keine unbedenkliche Reparatur, weil dadurch laut Projektdokumentation auch historische Teamzuordnungen verändert werden können.
**Fehlender Regressionstest:** Snapshot vor Definitionswechsel erzeugen, Zahlungsdatum abweichend nachtragen, danach Kachel, Liste und CSV auf gleiche Mitgliedschaft prüfen.
### F3 – Mittlere Priorität: Pausenende bleibt nach Änderung des Liefertermins auf dem alten Datum
**Commits:** Zusammenspiel von `ec3ecad` und `66c6ae0`
**Fundstellen:** [AboRepository.php](../../app/Repositories/AboRepository.php), Zeilen 99–105 und 243–249; [UserAbo.php](../../app/Models/UserAbo.php), Zeilen 135–149 und 171–175.
Eine Pause setzt `paused_until` auf den neuen Liefertermin. Eine nachfolgende zulässige Änderung des Liefertags schreibt jedoch nur `abo_interval` und `next_date`. Der Explainer und der Admin-Filter vertrauen weiterhin auf das alte `paused_until`.
**Reproduziert:** Nächste Lieferung 05.10.2026; Pause verschiebt sie auf 05.11.2026. Danach wird der Liefertag auf den 20. geändert. Ergebnis: `next_date = 20.11.2026`, aber `paused_until = 05.11.2026`.
**Auswirkung:** Kunden und Mitarbeitende erhalten einen falschen Wiederaufnahmetermin. Ab dem 06.11. verschwindet das Abo aus dem Pausenfilter, obwohl die verschobene Lieferung erst am 20.11. ansteht. Bei einer Vorverlegung kann die Anzeige umgekehrt eine längere Pause versprechen als tatsächlich vorgesehen.
**Empfehlung:** Bei Terminänderungen während einer laufenden Pause das Pausenende konsistent mitführen oder die laufende Pause aus einer eindeutigen Quelle ableiten. Die Regel für Selfservice und Admin zentral umsetzen. Beim Abschluss einer Pause auch `status_reason` bereinigen: Der aktuelle Hook löscht bei geändertem `last_date` zwar `paused_until`, aber einen bestehenden `paused_by_*`-Grund bei unverändertem Status 2 nicht.
**Fehlender Regressionstest:** Pause → Liefertermin nach hinten/vorn ändern → Erklärung und Admin-Filter prüfen → erfolgreiche Ausführung → Pausenfelder und Pausengrund entfernt.
### F4 – Mittlere Priorität: Neuer Test erwartet eine nicht mitcommittete Controller-Erweiterung
**Commit:** `d5ad1ba`
**Fundstellen:** [TeamFirstlinesPageTest.php](../../tests/Feature/TeamFirstlinesPageTest.php), Zeilen 69–72; [TeamController.php](../../app/Http/Controllers/User/TeamController.php), Rückgabedaten in `firstlines()`, im Commitstand ab Zeile 752.
Der Test erwartet neu `expiredCount`. Der Controller liefert diesen Schlüssel im Git-Stand nicht. Das Ergebnis ist reproduzierbar `Undefined array key "expiredCount"`.
**Besonders relevant:** Die passende Controller-Erweiterung existiert im aktuellen Arbeitsverzeichnis bereits als uncommittete Änderung. Damit kann ein Test im gemischten Arbeitsverzeichnis einen vollständigeren Stand prüfen, als später aus den vier Commits ausgeliefert wird.
**Empfehlung:** Die fachlich zusammengehörende Controller-/View-/Test-Änderung vollständig und fokussiert versionieren oder die sachfremde Test-Erweiterung aus diesem Paket herauslösen. Die Abnahme anschließend gegen einen sauberen Checkout durchführen.
**Nachweis:** Dies ist der einzige zusätzliche fehlgeschlagene Test im vollständigen Vergleich mit `203999a`.
### F5 – Niedrige Priorität: Backfills verändern entgegen der Dokumentation `updated_at`
**Commits:** `ec3ecad`, zusätzlich entsprechendes Verhalten in `d5ad1ba`
**Fundstellen:** [AboBackfillStatusReasons.php](../../app/Console/Commands/AboBackfillStatusReasons.php), Zeilen 42–43 und 65–69; [UserUtil.php](../../app/Services/UserUtil.php), Zeilen 330–331.
Der Abo-Backfill behauptet, ohne Änderung von `updated_at` zu schreiben. Tatsächlich verwendet er Eloquent-Builder-Updates; diese ergänzen den Aktualisierungszeitstempel. Auch `saveQuietly()` im Partner-Backfill unterdrückt Events, aber keine Zeitstempel.
**Reproduziert:** Ein bestehendes Abo mit `updated_at = 01.08.2026 12:00:00` erhält beim Status-Backfill den Testzeitpunkt `16.09.2026 10:00:00`.
**Auswirkung:** Die ursprüngliche Änderungszeit geht für betroffene Datensätze verloren. Sortierungen oder spätere Analysen anhand dieses Feldes sehen eine Massenänderung zum Deploy. Eine darüber hinausgehende konkrete Störung wurde nicht nachgewiesen.
**Empfehlung:** Bewusst ohne automatische Zeitstempel schreiben, beispielsweise mit geeignetem Query-Builder-Update oder `withoutTimestamps()`. Den Erhalt der Zeitstempel testen und die Deploy-Dokumentation mit der tatsächlichen Implementierung abgleichen.
## Weitere Verbesserungen und Optimierungen
### Kündigungsbestätigung auch serverseitig prüfen
Das neue Formular sendet `abo_cancel_confirm`, aber kein PHP-Pfad wertet das Feld aus. Ein Request mit `abo_cancel=true` ohne Bestätigung kündigt weiterhin erfolgreich; dies wurde im Repository-Test reproduziert. Mindestlaufzeit und Sperrfrist werden dabei weiterhin geprüft – es handelt sich also nicht um deren Umgehung.
Wenn die Checkbox eine verbindliche Bestätigung der angezeigten Folgen sein soll, sollte der Selfservice-Pfad eine `accepted`-Validierung erhalten. Dazu Tests für fehlende, falsche und gültige Bestätigung ergänzen. Der normale Browserablauf wird aktuell lediglich durch den deaktivierten Button gesteuert.
### Zusätzliche Einzelabfragen im Status-Explainer reduzieren
[AboStatusExplainer.php](../../app/Services/Abo/AboStatusExplainer.php), `latestHistory()` ab Zeile 199, fragt die Historie bei jedem Aufruf erneut ab. Ein deaktiviertes Abo benötigt mindestens eine zusätzliche Abfrage für das Datum; ohne gespeicherten Akteur entstehen zwei Abfragen derselben Historie für Akteur und Datum. Das neue Partial wird auch pro Tabellenzeile gerendert.
Empfehlung: Die jeweils benötigten letzten Historieneinträge gesammelt laden, geladene Beziehungen tatsächlich verwenden und den Eintrag innerhalb einer Erklärung wiederverwenden. Ein Query-Count-Test mit beispielsweise 50 Abos ist hier aussagekräftiger als weitere reine Textvergleiche. Der Admin lädt die Abo-Bestellungen bereits vor; diese vorhandene Optimierung ist positiv.
### Herkunft und Genauigkeit rückgefüllter Zahlungsdaten sichtbar halten
`firstRegistrationPaidAt()` verwendet ohne Punktebuchung `shopping_orders.updated_at`, ohne passende Bestellung `active_date`. Beide Ersatzwerte können fachlich vom ersten Zahlungseingang abweichen: `updated_at` kann durch spätere Bestellbearbeitung verschoben werden, `active_date` wird unter anderem bei Wiederherstellung neu gesetzt. Diese Fallbacks sind dokumentiert, erscheinen in der Oberfläche aber uneingeschränkt als „Bezahlt am“.
Empfehlung: Eine belastbare Zahlungstransaktion bevorzugen und Ersatzwerte mit Herkunft beziehungsweise Unsicherheit kennzeichnen. Ein Dry-Run mit Zählung je Datenquelle verbessert die Kontrolle vor der Migration. Zudem sollte „erste Zahlung“ bei mehreren bezahlten Registrierungsbestellungen nach Zahlungszeitpunkt bestimmt werden; die kleinste Bestell-ID allein garantiert diese Reihenfolge nicht. Das tatsächliche Ausmaß dieser Randfälle wurde nicht gegen Bestandsdaten geprüft.
### Backfill von Schemaänderung entkoppeln und Datenzugriffe bündeln
Die Migration ruft einen veränderlichen Anwendungsservice auf, der pro Partner eine Bestellung, gegebenenfalls eine Punktebuchung und ein Update ausführt. Chunking begrenzt den Speicher, aber nicht die Zahl dieser Einzelabfragen. Ein eigenständiges, wiederaufnehmbares Backfill-Kommando mit Dry-Run, Statistik je Quelle und gebündelten Datenzugriffen wäre besser kontrollierbar. Den erstmaligen Schreibvorgang von `partner_paid_at` bei parallelen Zahlungsprozessen außerdem atomar absichern; der aktuelle Null-Check schützt nur die einzelne geladene Modelinstanz. Ein tatsächlicher Parallelitätsfehler wurde hier nicht reproduziert.
### Änderungen und Tests stärker nach Fachthema trennen
Der Statistik-Commit enthält zusätzlich Änderungen am Löschpräfix von E-Mail-Adressen (`buildDeletedEmail()` / `stripDeletePrefix()`). Dazu passende Tests liegen im Arbeitsverzeichnis untracked, sind jedoch nicht Teil des Commits. Diese Änderungen sollten mit eigener Beschreibung und Regressionstests gemeinsam versioniert werden. Dasselbe gilt für die Firstline-Erweiterung aus F4.
Für A1/A2 zusätzlich einen Integrationstest des tatsächlichen Ausführungscommands am 30. eines Monats mit Liefertag 31 ergänzen. Die neuen Tests belegen die Hilfsfunktion, aber nicht allein den gesamten Ablauf Auswahl → Bestellung → nächster Termin. Für A4 fehlt eine Browserprüfung der Checkbox, des Modal-Resets und des Buttons „Lieber pausieren“.
## Validierung
### Ergebnisse
| Prüfung | Ergebnis |
| --- | --- |
| PHP-Syntax aller 47 geänderten PHP-Dateien ohne Blade-Templates | Alle erfolgreich |
| Gezielte Tests zu Abo-Tickets, Zahlungsdatum, Prognose und Statistik | 163 Tests: **161 bestanden, 2 fehlgeschlagen**, 850 Assertions |
| Vollständige Suite auf `203999a` | **1.055 bestanden, 96 fehlgeschlagen, 1 übersprungen**, 4.019 Assertions |
| Vollständige Suite auf `d5ad1ba` | **1.095 bestanden, 97 fehlgeschlagen, 1 übersprungen**, 4.248 Assertions |
| Vergleich der vollständigen JUnit-Ergebnisse | Genau **ein neuer Fehlschlag**, keine zuvor fehlgeschlagene Prüfung behoben |
| Zusätzliche temporäre Reproduktionstests | **5 bestanden**, 15 Assertions; bestätigen bewusst die fehlerhaften Ist-Zustände |
Die zwei Fehlschläge der gezielten Auswahl sind `HomeMonthlyStatsTest` (`payline_is_career_fallback`, bereits in der Basis fehlgeschlagen) und `TeamFirstlinesPageTest` (`expiredCount`, neu durch `d5ad1ba`). Die 96 gemeinsamen Fehlschläge wurden nicht als durch diese Tickets verursacht bewertet. Die Zahlen gelten für die identische isolierte Testumgebung; sie sind keine Aussage über die Laufzeitfehlerzahl der Produktion. Zusätzlich meldet PHPUnit das bereits vorhandene veraltete XML-Konfigurationsschema.
Die fünf temporären Reproduktionen prüfen: unbezahlte Rechnung und spätere Zahlung, historische Snapshot-/Listenabweichung, veraltetes Pausenende nach Terminänderung, verändertes `updated_at` beim Status-Backfill und Kündigung ohne Checkbox-Bestätigung.
### Wiederholbare Testbefehle
In einem sauberen Checkout mit gültigem reinem Test-`APP_KEY`, Test-Zahlungskonfiguration und den vorhandenen Dependencies:
```bash
php artisan test --compact --filter='AboDeliveryDayTest|AboLockDaysSettingTest|AboPauseCancelSectionTest|AboPauseTest|AboRepositoryDateChangeTest|AboStatusExplainTest|PartnerPaidAtTest|PaylineForecastServiceTest|TeamFirstlinesPageTest|TopResultsServiceTest|BackofficeStatisticsDrilldownTest|BackofficeStatisticsListsTest|BackofficeSnapshotFreezeTest|HomeMonthlyStatsTest|StatisticsConsistencyTest|BackofficeDashboardServiceTest'
php artisan test --compact --log-junit /tmp/audit-results.xml
```
Beim Audit wurden für den Artisan-Boot zusätzlich `APP_ENV=testing`, `DB_CONNECTION=sqlite`, `DB_DATABASE=:memory:` und der in `phpunit.xml` hinterlegte Testwert für `PAYONE_KEY_HASH` gesetzt. Die vollständige HEAD-Prüfung schloss die separat markierte temporäre Testgruppe `audit` aus. Es wurden die installierten Dependencies verwendet, keine Paketaktualisierungen vorgenommen.
## Empfohlene Reihenfolge
1. F1 korrigieren und den Rechnungskauf über einen Monatswechsel absichern.
2. F2 mit einer konsistenten Strategie für bestehende Snapshots lösen; keine ungeprüfte historische Neuberechnung.
3. F3 korrigieren und die Kombination Pause/Terminänderung/Ausführung testen.
4. F4 durch einen vollständigen, fokussierten Commit beheben und den sauberen Checkout erneut testen.
5. F5 vor dem Backfill bereinigen; anschließend Bestätigungsprüfung und Performance-Verbesserungen umsetzen.
Die bereits dokumentierten Deploy-Schritte bleiben relevant: Schema-Migrationen, Abo-Status-Backfill zunächst als Dry-Run, beide Sperrfrist-Settings ausdrücklich auf `0` und Blade-Views leeren. Ob diese Schritte auf einer Zielumgebung bereits erfolgt sind, war nicht Gegenstand dieses Audits.
---
## Stellungnahme und Umsetzung (18.09.2026)
Jeder Befund wurde gegen den Code geprüft. Ergebnis: **alle fünf Befunde und die Kündigungsbestätigung sind
zutreffend** und wurden umgesetzt (Commit „Audit 18.09.“ auf `payment/phase-r-payone-refactoring`). Die
Zahlen der Validierung wurden nicht nachgerechnet; der eine neue Fehlschlag (F4) ist reproduzierbar und
behoben.
| Befund | Bewertung | Umsetzung |
| --- | --- | --- |
| F1 Rechnungskauf setzt Zahlungsdatum | **zutreffend** — `paymentStatusPaidAction($order, false)` beim Rechnungskauf, `markPartnerPaid()` prüfte `$paid` nicht | `markPartnerPaid()` nur noch bei `$paid = true`; die spätere Rechnungszahlung (`ManualPaymentService`, `$paid = true`) setzt den Wert. Backfill: Aktivierungsdatum nur noch, wenn **gar keine** Registrierungsbestellung existiert — eine offene Rechnung bleibt leer. Tests: offene Rechnung Ende August, Zahlung Anfang September → September; Backfill mit offener Rechnung → leer. |
| F2 Kachel und Liste mit unterschiedlichen Regeln | **zutreffend** — Snapshot zählte nach damaliger Regel, Drilldown filterte die eingefrorenen Mitglieder mit der neuen Regel | Neue Snapshots speichern je Linie `new_partner_ids`; die Liste nimmt exakt diese. Alte Snapshots ohne IDs: Liste rechnet mit der damaligen Regel (`scopeNewPartnerInByActiveDate`, Registrierungsmonat) über die eingefrorenen Mitglieder — Kachel und Liste stimmen wieder überein, ohne historische Neuberechnung. Kein Bump der Payload-Version (alte Snapshots bleiben gültig). Tests: nachgetragenes Zahlungsdatum, alter Snapshot ohne IDs. |
| F3 Pausenende bleibt bei Terminänderung stehen | **zutreffend** | Liefertag-/Termin-Änderung (Selfservice und Admin) zieht `paused_until` auf den neuen Termin, solange eine Pause läuft. Ausführung (`last_date` ändert sich) löscht jetzt auch den `paused_by_*`-Grund. Test: Pause → 5.11. auf 20.11. → Admin auf 10.12. → Ausführung → Felder leer. |
| F4 Test ohne Controller-Änderung committet | **zutreffend** — `expiredCount` lag nur im Arbeitsverzeichnis | Controller-Hunk, View und die vier `team.php`-Keys zusammen versioniert. |
| F5 Backfills schreiben `updated_at` | **zutreffend** — Eloquent-Builder-`update()` ergänzt den Zeitstempel, `saveQuietly()` ebenfalls | Abo-Backfill über `toBase()->update()`, Partner-Backfill mit `timestamps = false`. Tests prüfen den erhaltenen Zeitstempel. |
| Kündigungsbestätigung nur im Browser | **zutreffend** | `abo_cancel_confirm` wird im Selfservice serverseitig verlangt (nach Mindestlaufzeit und Sperre), sonst `abo.cancel_confirm_required`. Admin unverändert. Tests: fehlend, `0`, `1`. |
| Explainer-Einzelabfragen | **zutreffend** | Historie je Abo und Aktion nur einmal geladen, vorgeladene Relation wird genutzt. Kein Query-Count-Test; Listen laden `status_histories` weiterhin nicht vor (offen, siehe unten). |
**Nicht umgesetzt, bewusst:**
- *Herkunft der rückgefüllten Zahlungsdaten kennzeichnen* und *Backfill von der Migration entkoppeln*: Die
Migration `2026_09_16_090000` mit Backfill ist Teil von `d5ad1ba`; ob sie auf Produktion schon gelaufen ist,
war hier nicht feststellbar. Eine Kennzeichnung der Quelle bräuchte eine weitere Spalte — Aufwand ohne
fachliche Anfrage. Für die Erstzahlung bei mehreren bezahlten Registrierungsbestellungen gilt weiter die
kleinste Bestell-ID.
- *Atomares Erstsetzen von `partner_paid_at`*: Der Paid-Pfad läuft in beiden Webhooks bereits in einer
Transaktion mit `lockForUpdate` auf der Bestellung (C2); ein zweiter Zahlungsprozess desselben Users im
selben Moment ist nicht reproduziert worden.
- *Integrationstest des Ausführungscommands am 30. mit Liefertag 31* und *Browserprüfung A4*: offen.
- *Löschpräfix-Änderung (`buildDeletedEmail`) im Statistik-Commit*: gehört zum DSGVO-Thema, das noch
uncommittet im Arbeitsbaum liegt; wird mit diesem Thema versioniert.
**Deploy-Folgen:**
- Keine neue Migration. `php artisan view:clear`.
- Falls `d5ad1ba` bereits auf Produktion lief: Rechnungskäufe seit diesem Deploy haben ein falsches
`partner_paid_at` (Datum des Rechnungskaufs statt der Zahlung). Prüfen und leeren:
```sql
-- Kandidaten: Rechnungskauf offen, keine bezahlte Registrierungsbestellung
SELECT u.id, u.email, u.partner_paid_at
FROM users u
WHERE u.partner_paid_at IS NOT NULL
AND EXISTS (SELECT 1 FROM shopping_orders o WHERE o.auth_user_id = u.id AND o.payment_for = 1 AND o.paid = 0)
AND NOT EXISTS (SELECT 1 FROM shopping_orders o WHERE o.auth_user_id = u.id AND o.payment_for = 1 AND o.paid = 1);
-- danach fuer diese IDs: UPDATE users SET partner_paid_at = NULL WHERE id IN (...);
```
Der Wert wird bei der Rechnungszahlung neu gesetzt. Die Backfill-Logik nimmt für diese Fälle nicht mehr
das Aktivierungsdatum.
- Snapshots müssen nicht neu gebaut werden; ab dem nächsten Monatsabschluss enthalten sie die
Neupartner-IDs. Ein `--force`-Neuaufbau bleibt eine gesonderte Entscheidung (siehe
`neupartner-zahlungsmonat.md`).

View file

@ -1,6 +1,6 @@
# Entwicklungsplan – Protokoll Zoom-Meeting 08.09.2026
**Stand:** 16.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
**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.
**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)