- Neue Seite Anerkennungen (/user/recognitions, Menue unter News Archiv) fuer
alle Berater mit aktivem Konto: oben die Anerkennungen des Vormonats in den
fuenf Kategorien der Monatsmail, Filter zurueck bis Januar 2026; darunter
Live-Ranglisten (Top 10 je Kategorie) des laufenden Monats, 15 Min. Cache.
- Kategorien: Top-Recruiter (ab 2 Neupartnern), neue Kunden-Abos,
Kundenabo-Punkte, Kunden-Abos gesamt, Gesamtpunkte (1.000-Punkte-Shop,
mit Platz 4). Regeln wie TopResultsService/VIP-Statistik, nur firmenweit.
- Firmenkonto (6) und Grosskundin (3) nie aufgefuehrt:
config mivita.recognition_excluded_user_ids.
- Abgeschlossene Monate sind bis zum 3. des Folgemonats vorlaeufig und werden
dann eingefroren: neue Tabelle recognition_snapshots, Command
recognitions:freeze (taeglich 06:15, idempotent, holt offene Monate nach).
- September-Anerkennungen firmenweit geprueft und korrigiert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- E-Mail-Aenderung durch Berater wird erst nach Bestaetigung durch den Kunden
aktiv (Link an die neue Adresse, Portal kunde/email-bestaetigen); danach
ziehen alle Eintraege des Kunden und seine Abos mit. Admins aendern direkt.
- Kundenhoheit: E-Mail eines Beraterkontos oder eines Kunden eines anderen
Beraters wird abgelehnt (beim Speichern und bei der Bestaetigung).
- Fix CustomerPriority::entryExists: aelteste gepruefte Zuordnung gilt,
Eintraege in Pruefung (is_like) dienen nicht mehr als Vorlage.
- "Berater zuweisen" voreingestellt auf alle Bestellungen des Kunden.
- Neue Admin-Seite Kundenhoheit: offene Pruefungen, Kunden bei mehreren
Beratern, abgelehnte/offene E-Mail-Aenderungen; Zaehler im Menue.
- Neue Tabelle shopping_user_email_changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Kontaktformular: keine Mail mehr an die eingegebene Adresse (wurde als
Spam-Versender missbraucht); Anfrage nur an Kontakt-Adresse + Berater (BCC),
Reply-To = Absender; Eingaben in der Mail escaped
- Kontakt + Registrierung: Cloudflare Turnstile statt reCAPTCHA (Schluessel
aus dem Code in die .env), Rate-Limits, Honeypot + Mindest-Ausfuellzeit,
Namensregel (lateinische Schrift, max. 50 Zeichen), Form Requests
- Registrierung: Partner-Nummer nur fuer aktive Berater (vorher ungeprueft)
- CRM: POST /register abgeschaltet (legte ohne Captcha Benutzer an)
- Passwort-Mindestlaenge systemweit 8 Zeichen ueber Password::defaults()
- Log abgefangener Anfragen: storage/logs/form-spam-*.log
- Doku und Protokoll: dev/2026-10-09-spam/README.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 50 EUR je Neupartner bleiben; die Top 3 (500/300/200 EUR) ermittelt
jetzt der Umsatz der Neupartner (wie Montenegro, ohne Einmalpunkte/Abos)
- Gleichstand bei Umsatzwertung: vorn liegt, wessen letzte gezaehlte
Umsatzbuchung frueher kam
- Challenge-Seite: Praemie und Top-3-Darstellung auch bei Umsatzwertung,
Ranglisten-Hinweis auf Umsatz (DE/EN/ES/FR)
- Seeder: neue Texte und Bedingungen (Rangliste, Steuerpflicht,
vergünstigter Einstieg bis 31.10.), gleicht Wertung beim Einspielen ab
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- page_script in invoice, delivery, credit und cancellation ans Ende des
body verschoben (wirkte nur auf Seite 1), Beschriftung uebersetzt
- Gutschriften rendern mit enable_php wie Rechnungen
- Lieferschein: Schluessel pdf.delivery_note_no ohne ueberzaehligen Punkt
- Tests: Position des Skripts, alle pdf.*-Schluessel in allen Sprachen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Patch-/Minor-Updates (guzzle, commonmark, flysystem, excel,
phpspreadsheet, phpseclib, laravel/framework 11.x)
- barryvdh/laravel-dompdf 3.1 (dompdf 3.1.6), Test fuer den PDF-Weg
inkl. eingebettetem PHP und Briefpapier
- Rueckport des Laravel-12.60-Fixes gegen CRLF in der email-Regel
- Protokoll: Ergebnis composer audit, offene Punkte Laravel 12 und
PDF-Seitenzahlen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sentry/sentry-laravel 4.28; config/sentry.php gehaertet (keine PII,
keine Request-Bodys, before_send ErrorReportScrubber)
- Handler: Integration::captureUnhandledException, CommandNotFoundException
nicht melden
- ReportErrorSignals (global): toter interner Link (nach Pfad wegen
Catch-all-Route), 403/429 fuer Angemeldete, 419; gedrosselt
- Browser: selbst gehostetes SDK (esbuild-Bundle 11.5.0), Loader ohne
Build-Schritt und ohne Regex-Lookbehind, in 7 Layouts mit area-Tag
- phpunit.xml: DSNs und ntfy-URL leer, CACHE_DRIVER=array (Tests schrieben
bisher in den lokalen Datei-Cache)
- dev/glitch-tip/README.md mit Instanz, Org, Projekt-IDs, API-Auswertung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dev/betrieb/2026-10-08-protokoll-umstellung.md: Ergebnis, Releases,
Aufraeumen ab 22.10.2026 (Server, Mac, Git-Server), offene Punkte
- Kernel: abo:send-execution-reminder ist bewusst aktiv (veralteten
Kommentar entfernt)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- HealthProbe: Datenbank, Cache, Queue (sync / database / Horizon),
fehlgeschlagene Jobs (24 h), Scheduler-Herzschlag, Plattenplatz
- /health auf jeder Domain, ohne web-Gruppe, nur mit MONITORING_HEALTH_TOKEN
- monitoring:heartbeat minuetlich im Scheduler
- Sysadmin -> Systemstatus (my.mivita.care/sysadmin/status) mit Version,
Umgebung und zuletzt fehlgeschlagenen Jobs
- Anleitung Abschnitt G
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Supervisor bedient alle Queues der Jobs (high-priority, default, dhl-*),
production mit 3 statt 10 Prozessen
- Redis-Queue mit after_commit: der Stripe-Paid-Pfad plant den
Rechnungsnummer-Sync innerhalb der Transaktion ein
- /horizon nur fuer System-Admins (admin >= 4)
- horizon:snapshot alle 5 Minuten, queue:prune-failed nach 30 Tagen
- deploy.sh: QUEUE_WORKER (horizon | none), Anleitung Abschnitt F
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dev/betrieb/deploy.sh: Tag auschecken, composer, Caches leeren (bewusst
kein route:/config:cache wegen hostabhaengiger Routen und env()-Aufrufen),
horizon:terminate, FPM-Reload, Worker-Pruefung am Ende
- dev/betrieb/README.md: Einrichtung Server, Umstellung des Live-Verzeichnisses,
Release, Deploy, Rollback
- config('app.version') aus RELEASE, php artisan about --only=mivita
- monitoring:queue-probe prueft nach dem Deploy, ob Horizon Jobs abholt
- Vorlage dev/git-deploy-monitoring aus Pressekonto
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kunden loeschen (Rueckmeldung "es wird nichts geloescht"):
- DSGVO-geloeschte Eintraege blieben mit Name in den Listen stehen; Kunde
61009 wurde deshalb achtmal geloescht, bei Eintrag 45013 wurde die E-Mail
danach ueber ein offenes Bearbeiten-Formular zurueckgeschrieben
- neue Spalte shopping_users.gdpr_deleted_at (Bestand per Migration markiert)
- Admin-Kundenliste blendet geloeschte Kunden aus (Filter: ausblenden /
mit anzeigen / nur geloeschte), Berater-Kundenliste immer
- Bearbeiten/Speichern im Admin und beim Berater gesperrt; ShoppingUser
verweigert das Zurueckschreiben von E-Mail und Telefon
- Modal und Erfolgsmeldung (de/en/es/fr) sagen klar, dass Name und
Rechnungsadresse wegen der Aufbewahrungspflicht bleiben
- gdpr:repair-customers [--execute] zieht protokollierte Loeschungen nach
- DSGVO-Loeschung prueft Superadmin serverseitig; Berater duerfen eine
Kunden-E-Mail nur beim eigenen Kunden nachtragen
- setFakerMail setzt faker_mail bei geloeschten Eintraegen nicht zurueck
Checkout: "Wie bist du auf uns aufmerksam geworden?" ist kein Pflichtfeld
mehr und wird farbig hervorgehoben.
Deploy: php artisan migrate && php artisan gdpr:repair-customers --execute
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Berater ohne UserShop liefen bei Kundenbestellungen (abo-ot-customer) in
"Attempt to read property id on null". Shops werden jetzt bei Freischaltung
und bei Bedarf automatisch angelegt (Name aus Vor-/Nachname, bei Belegung
-2, -3, ...). Nachtrag fuer Bestandsberater: php artisan user:ensure-shops.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kundenentscheid 25.09.2026 zu den Regeln 4 bis 7:
- Firmenkonto (mivita.company_user_id) steht in keiner Rangliste und
bekommt keine Praemie; getrackt wird weiter, damit ein ueber den
Firmenlink registrierter Partner nach dem Umhaengen mit umzieht
- Bedingungen Punkt 4 (de/en/es/fr): auch Kunden, die Berater werden,
zaehlen; massgeblich ist der aktuell hinterlegte Sponsor
- Test: Anmeldung ueber den Firmenlink, Umhaengen, Wertung beim neuen
Sponsor
- Doku: Regeln 4 bis 7 entschieden
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Kundenentscheid 25.09.2026:
- Storno: Ist jede bezahlte Starterpaket-Bestellung eines Neupartners
storniert (txaction = cancelled), bekommt das Tracking cancelled_at,
eine Storno-Logzeile nimmt die Einmalpunkte zurueck. Nachtlauf und
Neuaufbau markieren; "Meine Berechnung" zeigt den Partner durchgestrichen
mit "Storniert am ..., zaehlt nicht".
- Gezaehlt wird fuer alle, einen Rang bekommen nur bestaetigte Teilnehmer;
Export und Summen nur fuer sie. Unbestaetigte sehen "Deine Praemie nach
Bestaetigung", im Admin "Teilnahme nicht bestaetigt, keine Auszahlung".
- Datenschutz: andere Berater sehen Unbestaetigte in der Rangliste nicht
mehr (auch nicht anonym), VIP-Konten schon, der eigene Eintrag bleibt.
- Teilnahmebedingungen Punkt 2 und 7 und Hinweistexte in de/en/es/fr.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Der Dry-Run auf Produktion (25.09.) listet einen Berater, der ohne Sponsor
registriert wurde (m_sponsor und pre_sponsor leer). terminate() haette ihn
jede Nacht mit "kein aktiver Sponsor" uebersprungen. Pausieren und harter
Cut nehmen jetzt den naechsten aktiven Sponsor ueber m_sponsor/pre_sponsor
und sonst das Firmenkonto (ohne Mail).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Die Recruiting Challenge erfasste Eigen- und Kundenabos mit 0 Punkten
(Live-Kopie: 175 Eigenabos). Sie tauchten in Details und Admin auf und
brachten Berater ohne Neupartner in die Rangliste.
- incentives.include_abos (Standard an); Migration schaltet Incentives
ohne jede Abo-Wertung aus (0 Abo-Punkte, 0 Mindestabos, ohne Umsatzpunkte)
- aus: kein Abo-Tracking in Tracker, Neuaufbau und Reparatur;
incentive:calculate raeumt vorhandene Abo-Zeilen ab
- Admin: Checkbox im Formular, Abo-Punkte/Mindestabos werden dann auf 0
gesetzt; Abo-Spalte und -Bereiche in Admin, Details und Teilnahme-Hinweis
entfallen
- Seeder der Recruiting Challenge setzt den Schalter aus
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
user:make_abo_order nimmt nur Abos mit next_date = heute; faellt ein Lauf
aus, bleibt ein Abo stillschweigend liegen (Abo 39: Termin 25.03.2026).
abo:check-overdue meldet taeglich um 05:30 alle aktiven Abos mit Status
okay und verstrichenem Termin per Mail an mivita (Link ins Admin-Abo) und
ins abo_order-Log. Es wird bewusst nichts nachgebucht; --no-mail nur fuer
die Ausgabe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Entwicklungsplan September, B2 (Protokoll 08.09., Rueckmeldung 14.09., R3):
- Ablauftag + 2 Kalendertage (Nachtlauf): Team, Kunden, Kundenabos und
Portal-Logins gehen umkehrbar an den naechsten aktiven Sponsor, jede
Umhaengung in consultant_suspension_transfers protokolliert. Kurzinfo
an den Sponsor ohne Listen, keine Mail an das Firmenkonto.
- Zahlung bis Tag 27: Berater aktiv, Team und Kundendaten zurueck (nur
was noch beim Sponsor liegt); Umsaetze der Zwischenzeit bleiben dort.
- Tag 28: was noch am Berater haengt geht endgueltig an den Sponsor,
Berater wird dessen Kunde (ohne Duplikat) und geloescht; Listen-Mail
mit Vorname, Nachname, Telefon, E-Mail an jeden haltenden Sponsor.
- Cron, Zahlungseingang und Admin-Ablaufdatum laufen ueber
ConsultantSuspensionService; user:cleanup --dry-run listet beide Stufen.
- Mail 35 und Dashboard nennen Pausentag und Frist; Fristen in
config/mivita.php (cleanup_suspend_days, cleanup_delete_days).
- Nebenbefunde: Kernel referenziert UserCleanUp, user:restore aus der
User-Bereinigung laeuft nur noch einmal.
- Testschema: shopping_users.auth_user_id nullable wie auf Produktion
(Migration aendert nur, wo die Spalte noch NOT NULL ist).
- Uebersicht fuer die Kundin: dev/_tickets/2026-09-23-mitgliedschaft-
ablauf-uebersicht.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
paylineContext() nahm die gesamte Downline, lineByUserId() und die
Team-Statistik den Provisionsbaum. "Teampartner/Abos in der Payline" und
die Payline-Prognose zaehlten damit auch Berater ohne Mitgliedsebene oder
Abrechnungskonto samt ihrem Unterbaum, deren Abo-Punkte nie in die
Provision eingehen. An der Live-Kopie betrifft das 102 von 162 Beratern
mit Payline. Nach dem Deploy cache:clear (payline_context_*, 6 h).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Rangliste: bei gleichen Punkten bestaetigte Teilnahme vor anonym, danach
wer den Stand zuerst erreicht hat (letzter zaehlender Neupartner bzw.
Neuabo laut Tracking-Tabellen), zuletzt die Teilnehmer-ID. Vorher
entschied bei Gleichstand die Datenbankreihenfolge
(Teilnahmebedingungen Recruiting-Challenge, Punkt 6).
- Neupartner zaehlen fuer Incentives ab ihrer ersten Kontozahlung
(users.partner_paid_at), wie in der Statistik (Kundenentscheid
16.09.2026), nicht ab dem Bestelldatum: Live-Tracking, naechtliche
Reparatur und Neuaufbau nutzen dieselbe Regel
(IncentiveTracker::newPartnerCountedAt). Wer am 29.09. bestellt und am
01.10. zahlt, zaehlt in der Challenge.
Montenegro ist abgeschlossen; gegen die Live-Kopie tauschen dort nur
Plaetze ab 81, und der naechtliche Lauf entfernt keine bestehenden
Trackings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Admin-Bereich "Newsletter-Listen" (Plan dev/_tickets/entwicklungsplan-
newsletter-listen.md): Berater und Kunden nach Filtern zusammenstellen,
als Liste speichern und wieder laden, Export als XLSX/CSV. Jeder Export
wird protokolliert (newsletter_list_exports).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ticket dev/_tickets/2026-09-08.md #1: neuer Punkt "Berater uebertragen" im
Superadmin (/admin/user/transfer). Vorschau zeigt Firstline, Downline,
geparkte Sponsor-Bezuege, Kunden, Kundenabos und Portal-Logins; nach
Bestaetigung wird alles in einer Transaktion umgehaengt und wie die
User-Bereinigung protokolliert (user_clean_up_logs,
shopping_user_member_logs, SysLog). Bestellungen, Umsaetze und
Provisionsdaten bleiben beim bisherigen Berater. Ziel unterhalb der
Firstline der Quelle wird abgelehnt (Kreis im Baum).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ticket dev/_tickets/2026-09-08.md #2:
- "Kunde loeschen" oeffnet ein Modal statt direkt per GET zu loeschen. Es
zeigt alle Eintraege desselben Kunden (Kundennummer oder E-Mail, ohne
Platzhalter-Adressen) mit Anzahl Bestellungen.
- Ohne Bestellungen: Komplett-Loeschung (anonymisiert und soft-geloescht,
Portal-Logins und Stripe-Kunden entfernt, Zahlungs-Tokens inaktiver Abos
geleert). Mit Bestellungen nur DSGVO-Loeschung (Superadmin); die arbeitet
jetzt ebenfalls auf der E-Mail-Gruppe und entfernt Portal-Logins.
- Bei aktivem Abo oder Berater-Datensatz wird abgelehnt.
- Die alte "Loeschung" (CustomerRepository::deleteCustomer) entfaellt.
- Fehlermeldungen nutzen alert-error; alert-danger zeigte kein Layout an.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Die Pakete stehen im Plan als umgesetzt und sind live, der Controller- und
View-Teil lag aber nur im Arbeitsbaum (Sprachkeys und Model-Methoden sind
seit 5a38f4a/685f7a9 versioniert):
- K5: Abo-Bestellungen in Punkteuebersicht, Dashboard-Punkten und eigener
Statistik tuerkis mit Zusatz "(Abo)", Legende darueber.
- K4: Summe der Payline-Abos ("149 + 391 = 540 Abos gesamt").
- Home: payline_is_career_fallback, solange die Monatsquali aussteht
(Kundenentscheid 01.09.); die View wertet das Kennzeichen schon aus.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Eine spaeter stornierte Rechnung fehlte im Export ihres Rechnungsmonats,
weil cancellation = true auch an der Originalrechnung steht. Sie bleibt
jetzt als Erloes im Rechnungsmonat; die Stornierung wird im Monat des
Stornobelegs gegengebucht (nur Belege ohne cancellation_id).
- Pruefliste je Buchungsstapel als CSV (Button im Steuerberater-Bereich):
Belege, die sich ohne Zusatzinformation nicht abstimmen lassen (doppelte
Rechnungsnummern, Stornos ausserhalb des Systems).
- Korrespondenz und offene Differenzen mit dem Steuerberater unter
dev/2026-07-17-steuern/.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- shopping_orders.shipped_at (+ Herkunft live/dhl/geschaetzt): gesetzt im
Model, sobald der Versandstatus auf versendet wechselt, egal an welcher
Stelle der Status gesetzt wird. sales:backfill-shipped-at traegt
Altbestellungen aus dem DHL-Label nach oder schaetzt.
- SalesVolumeService rechnet Absatzmengen je Produkt wahlweise nach
Versand (inkl. externer/API-Bestellungen, Stornos im Monat des
Stornobelegs) oder nach Rechnung. Sets werden fuer die zweite Liste in
Einzelartikel aufgeloest.
- Admin-Seite: Umschalter Versand/Rechnung, Datenqualitaet (Anteil
geschaetzter Versanddaten), Export Produkte und Einzelartikel.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Produkte mit 7,5 Punkten wurden an drei Stellen zu ganzen Zahlen:
user_abo_one_time_items (PHP-Cast int, 7,5 -> 7), homeparty_user_order_items
(MySQL rundete auf 8) und shopping_collect_orders (Summe der Shop-API).
Die Migration zieht die drei Spalten auf DECIMAL(10,2) nach (Nachtrag zu
2026_01_22), Cast und Abo-Snapshot rechnen mit float.
points:report-decimal-diff wertet die Differenzen je Berater und Monat aus
(nur lesend), points:repair-decimal korrigiert den laufenden Monat und
zieht die Monatssummen nach.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Slim::storeCroppedImage() vertraute dem vom Client gemeldeten Typ und
uebernahm die Endung aus dem Dateinamen; "shell.php" mit Typ image/png
landete als .php im public-Disk. Jetzt entscheidet
getimagesizefromstring() ueber die Bytes, die Endung kommt aus dem
geprueften Typ.
- Foto loeschen war ein GET-Link und liess sich per fremdem <img src>
ausloesen (CSRF); jetzt POST-Formular mit Token.
- Ohne Konto (users.account_id) bricht Upload/Loeschen mit 404 ab statt
mit einem Fatal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Log-Analyse 26.08. (P1-3): user:cleanup scheiterte tagelang an derselben
E-Mail-Kollision, weil "delete-<mail>" beim zweiten Loeschen derselben
Adresse doppelt vorkam. UserUtil::buildDeletedEmail() haengt die User-ID an
(seit b7e8bbd); die Wiederherstellen-Schaltflaeche entfernt jetzt auch das
neue Praefix (stripDeletePrefix) statt nur "delete-". Tests fuer beide
Formate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Log-Analyse 26.08. (P0-2, P1-5, P2-3):
- Produktcode fuer DHL Paket International ist V53WPAK; mit V53PAK wurde
kein einziges Auslandslabel erzeugt. Alte Codes werden gemappt
(LEGACY_PRODUCT_CODE_ALIASES), Abrechnungsnummer und Masse suchen auch
unter den alten Setting-Schluesseln (dhl_account_v53pak) und Env-Namen.
- Zu lange Felder (Name, Stadt, Telefon, Referenz) werden auf die
DHL-Grenzen gekuerzt bzw. bereinigt statt den Versand abzulehnen; refNo
mindestens 8 Zeichen, sonst die Bestellreferenz.
- Tracking wiederholt bei HTTP 5xx der Unified-API bis zu dreimal.
Zolldaten fuer die Schweiz (P0-3) sind nicht Teil dieses Commits.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Log-Analyse 26.08. (P1-4): Adressen wie "ruediger@..." mit Umlaut im
lokalen Teil lehnt der Mailer ab; Checkout-Bestaetigung und
Abo-Erinnerung scheiterten. Registrierung, Checkout, Kunden- und
Profilformulare pruefen jetzt mit email:filter. Die Abo-Erinnerung
ueberspringt bestehende Adressen dieser Art mit einem Log-Eintrag statt
einer Transport-Exception.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Log-Analyse 26.08. (P1-1, P2-1, P2-7, P2-8):
- MyLog schrieb in den nicht definierten Kanal userabo (109 EMERGENCY-
Eintraege, laravel.log rotierte nicht mehr); jetzt abo_order.
- MyLog verschickt die Benachrichtigung ueber die Queue, ein Mailfehler
bricht den Zahllauf nicht mehr ab. MailLog sammelt Nutzer, User-Agent
und Zeit beim Erzeugen ein, weil der Worker keinen Request hat.
- Abgelehnte OAuth-Anfragen (falscher grant_type) werden als Warnung
geloggt statt als Fehler gemailt.
- payments:check-accounts loggt je Berater nur noch auf debug und schreibt
am Ende eine Zusammenfassung (geprueft, versendet, uebersprungen).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Die Dateien lagen im Arbeitsbaum formatiert und inhaltlich geaendert
zusammen. Dieser Commit enthaelt nur die Pint-Formatierung des alten
Stands, damit die folgenden Commits nur noch Inhalt zeigen.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Eine Seite unter /incentive/{slug}: oben Teilnahme-Block mit Bedingungen
(oder fuer bestaetigte Teilnehmer der Kopf mit Rang, Punkten, Praemie und
Bedingungen zum Aufklappen), darunter Titelbild, Text, Kacheln, "So
funktioniert es", Galerie und das Live-Ranking mit Sprungmarke #ranking.
Die alte URL /incentive/{slug}/ranking leitet dorthin um, Dashboard und
Detailseite verlinken auf die Sprungmarke, show.blade.php entfaellt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Titelbild und Galerie liegen jetzt in public/img/incentive/<slug>/, der
Hauptordner ist nur noch Fallback fuers Titelbild. Damit erscheinen die
Montenegro-Impressionen nicht mehr unter der Challenge; die Fotos sind in
montenegro-incentive-2026/ verschoben.
Teaser: Call to Action "Bist du bereit fuer die Challenge?" mit Button ganz
oben. Bei Geldpraemie zeigen die Kacheln "Deine Praemie" (50 EUR je neuem
Teampartner) und "Top 3 Bonus" statt Incentive-Punkte, darunter der Abschnitt
"So funktioniert die Challenge". Rangliste: Button "Jetzt teilnehmen" wieder
in der Sekundaerfarbe.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Haken vip_only am Incentive: aktives Incentive erscheint dann nur fuer
VIP-Konten in Sidenav, Dashboard, Teaser, Rangliste und Details, andere
Berater bekommen 404; das Tracking laeuft fuer alle weiter. Seeder setzt den
Haken fuer die Abstimmung der Recruiting Challenge.
Ranglisten-Seite: solange die Teilnahme nicht bestaetigt ist, steht der
Teilnahme-Block mit Praemien-Chips, Checkbox, Button und aufklappbaren
Bedingungen ganz oben, darunter Trennlinie, Titel, Untertitel, Bild,
Beschreibung und die Info-Kacheln. Ueberschriften vergroessert, Abo-Bloecke
nur wenn Abos zaehlen.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Neue Felder amount_per_partner, bonus_rank_1..3 und include_sales_points an
incentives. Mit ausgeschalteten Umsatzpunkten zaehlt die Wertung nur die
Einmalpunkte je Neupartner, die Rangliste entspricht damit der Anzahl
Neupartner mit Starterpaket. IncentivePayoutService rechnet Neupartner mal
Betrag plus Rang-Bonus fuer qualifizierte Gewinner; Admin-Detailseite bekommt
Praemienspalte, Summen und XLSX-Export fuer die Auszahlung.
Sidenav und Dashboard zeigen alle aktiven Incentives, damit Montenegro und
die Challenge parallel laufen koennen. Teaser, Rangliste und Details zeigen
bei Geldpraemie die Praemien-Kachel und "Deine Praemie bisher" und blenden
Abo-Bloecke aus, wenn Abos keine Rolle spielen. Montenegro bleibt unveraendert.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
Wer Ende August registriert und am 1. September bezahlt hat, tauchte in keiner
Neupartner-Zaehlung auf: Der August-Snapshot wurde um 04:45 eingefroren, da
fehlte noch das Abrechnungskonto; im September lag active_date im falschen
Monat. Betroffen sind 2026 acht Partner, auch im Recruiting ihrer Sponsoren.
Kundenentscheidung 16.09.2026: der Zeitpunkt der Zahlung ist massgeblich.
Neue Spalte users.partner_paid_at, gesetzt bei der ersten Kontozahlung
(Payment::paymentStatusPaidAction) und bei manueller Freischaltung durch den
Admin — nur, solange sie leer ist; Verlaengerung und Wiederherstellung aendern
sie nicht. Bestehende Partner werden per Migration aus der ersten bezahlten
Registrierungsbestellung rueckgefuellt (Zeitstempel der Punktebuchung, ersatzweise
Bestelldatum, ohne Bestellung das Aktivierungsdatum). Bewusst nicht
payment_account minus ein Jahr: das ergaebe bei Verlaengerern den Jahrestag der
Erstzahlung.
User::scopeNewPartnerIn() und isNewPartnerIn() lesen jetzt partner_paid_at —
damit Kachel, Recruiter-Ranking, Top 3 und Neupartner-Liste gleichermassen.
Die Liste und der CSV-Export zeigen "Bezahlt am" neben "Registriert am".
Unveraendert: die Teamstruktur haengt weiter an active_date (auch im
Provisionsbaum). Wer ist neu dazugekommen und wer gehoert zur Linie sind zwei
verschiedene Fragen.
Die eingefrorenen Monate Januar bis August enthalten sieben dieser Partner
nicht; ob sie per --force neu gebaut werden, ist eine Entscheidung — siehe
dev/2026-07-22/neupartner-zahlungsmonat.md.
Mit drin: die handgebaute SQLite-Tabelle in BackofficeDashboardServiceTest
kannte status_reason aus d6efe56 noch nicht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A1: Jeder Monatstag ist als Liefertag erlaubt (UserAbo::allowedDeliveryDays),
Tage 29-31 liefern in kuerzeren Monaten am letzten Tag. Eine Stelle
AboHelper::dateForDayInMonth klammert; setNextDate, Erstausfuehrung,
Admin-Termin und Pause rechnen darueber, der Folgemonat immer aus dem
Liefertag (31.01 -> 28.02 -> 31.03). Liefertag-Aenderung gilt fuer den
laufenden Zyklus und nur nach vorn: ist der Tag im Referenzmonat vorbei,
wird abgelehnt, der Monat verschiebt sich nie (Rueckfrage R1).
Admin-Filter "naechste Ausfuehrung" als Datumsbereich. Prognose-Kacheln
bekommen die Kachel Monatsende, Team-Abos rechnen bis Monatsende.
A2: Sperrfristen Default 0, Setting 0 ist ausdruecklich 0 (fiel bisher auf
den Default zurueck). Bedingung Kalendertage <= Sperre, zentral in
AboHelper::isWithinLock: am Ausfuehrungstag ab 0:00 Uhr fixiert, Vortag bis
23:59 Uhr frei, fuer Liefertag, Produkte, Pause und Kuendigung (R2). Texte
mit _zero-Varianten ueber AboHelper::lockMessage.
Sprachdateien de/en/es/fr, neuer AboDeliveryDayTest, bestehende Tests
angepasst, Plan-Status mit Deploy-Schritt (Settings auf 0).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Neue Spalten user_abos.status_reason / paused_until und
user_abo_orders.error_message. Der Grund wird zentral am Model gepflegt
(saving-Event): Zahlungsfehler, Kuendigung/Deaktivierung mit Akteur aus dem
Formular-Kanal, Loeschen beim Zurueckkehren in den Lauf; die Pause setzt
paused_until und paused_by_*. Cron- und Retry-Zahlung schreiben den Fehlertext
des Anbieters an die Abo-Bestellung.
AboStatusExplainer uebersetzt das in einen Satz ("Pausiert bis ... vom Kunden
am ...", "Zahlung am ... fehlgeschlagen - Grund: ...", "Gekuendigt am ... vom
Berater"), Partial _status_explain in Abo-Detail (CRM, Portal, Admin),
Abo-Uebersicht, Kundenabo-Liste, Team-Kundenabos und Admin-Datatable; dort
neuer Filter "Zustand" (pausiert / Zahlung fehlgeschlagen). Backfill-Kommando
abo:backfill-status-reasons fuer Bestandsdaten, Fallback ueber das
Statusprotokoll und die Pausenfelder.
Sprachdateien de/en/es/fr, Tests in AboStatusExplainTest, Plan-Status.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Eigener Abschnitt auf der Abo-Seite (CRM me/ot und Kundenportal) mit
Fortschritt zur Mindestlaufzeit, Pause und Kuendigung. Die Kuendigung laeuft
ueber ein Modal, das die Folgen nennt (Abo endet, Konditionen entfallen, ein
neues Abo hat wieder 6 Pflichtlieferungen), die Pause als Alternative anbietet
und erst nach Bestaetigungs-Checkbox freigibt. Innerhalb der Sperrfrist vor
der Ausfuehrung erscheint statt der Buttons ein Hinweis
(AboHelper::isPauseCancelLocked). Das Einstellungs-Modal verweist nur noch
auf den Abschnitt. Die Kuendigung im Repository ist jetzt wie die Pause eine
eigenstaendige Aktion: Erfolgsmeldung statt Liefertermin-Hinweis, next_date
bleibt unangetastet.
Sprachdateien de/en/es/fr, Tests in AboPauseCancelSectionTest, Plan-Status.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
K11 aus dem September-Protokoll (Tickets C1 und C2 im Entwicklungsplan), live seit 14.09.2026.
C1 – Statistik rechnet wie die Provision
- Standard-Baum ist der Provisionsbaum (Mitglieder wie TreeCalcBot: Mitgliedsebene,
Abrechnungskonto, active_date; ausgefilterte Knoten kappen ihren Unterbaum, max. 21 Linien).
Gesamte Downline bleibt per Umschalter erreichbar, Wahl in der Session.
- Punkte-Definition: Provisionspunkte (TP + Shop), reine Karrierepunkte ausgewiesen statt
eingerechnet, negative Monatssalden je User wie in der Provision auf 0 gekappt.
- Abgeschlossene Monate nehmen die Mitglieder je Linie aus dem gespeicherten Provisionslauf
(user_businesses), Snapshots je Baum-Definition (Payload-Version 3, Migration).
- Struktur-Modal: Gesamt aus den Linien-Punkten mit Dezimalen statt der int-Spalte total_pp.
- Messlauf statistics:compare-trees (Downline / Provisionsbaum / Struktur je VIP, Laufstand-Drift).
C2 – Laufstand-Race (Provision zu niedrig bei zeitgleichen Buchungen)
- Neuberechnung liest die Zeilen des Beraters sperrend (FOR UPDATE), Index (user_id, year, month).
- Paid-Pfad der Payone- und Stripe-Webhooks als DB::transaction mit 3 Anlaeufen; Deadlock und
Lock-Timeout aus der Rechnungserstellung werden nicht mehr geschluckt.
- Sicherheitsnetz sales:repair-running-totals taeglich 02:45 vor dem Provisionslauf;
abgeschlossene Monate werden nicht angefasst (Entscheid 14.09.: keine Nachzahlung fuer 06/08).
Doku: Entwicklungsplan zum Protokoll, Deploy-Notiz 14.09. (inkl. Migrations-Stolperstein:
Migrationen tragen Werte als Literale, nicht als Konstanten aus dem Anwendungscode).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vorschau und Liste liessen sich nicht gegeneinander nachrechnen — zwei Ursachen,
beide hier behoben.
1. Die Vorschau nahm eine pauschale Monatssumme als Basis. Damit enthielt die
Kachel zum 5. auch schon die Lieferungen vom 10. Jetzt zaehlt je Termin, was
bis zu diesem Tag gebucht war, plus die bis dahin offenen Lieferungen.
Buchungen nach dem letzten Liefertag (Nachzuegler, manuelle Gutschriften)
haengen sich an die letzte Kachel, damit die den Monatsstand zeigt. Jede
Kachel weist ihre Aufschluesselung aus ("457 abgerechnet + 131 offen").
Beraterin 454 im September: 132 / 457 / 588 / 776 statt 0 / 0 / 131 / 319.
2. Die Liste zeigte nur den wiederkehrenden Abo-Inhalt. Einmal-Artikel und
sonstige Zubuchungen einer Lieferung werden nach der Ausfuehrung aus dem Abo
entfernt und tauchten deshalb nirgends mehr auf — bei Abo 266 waren das 43
Punkte, bei Abo 418 rund 39. Neue Spalte "Diesen Monat" mit den je Abo
tatsaechlich gebuchten Punkten, bei Abweichung mit dem Zusatz "+43 einmalig",
dazu eine Summenzeile. Ein nicht zuordenbarer Rest (geloeschtes Abo, Buchung
ohne Ausfuehrungsbezug) wird gesondert ausgewiesen, damit die Addition aufgeht.
Die Spalte erscheint nur ohne Monatsfilter; fuer einen vergangenen Monat waere
sie irrefuehrend.
Summe der Monatsspalte = Basis der letzten Kachel; Summe der Abo-Punkte der Abos
mit Termin 20./25. = deren offener Teil.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Kachelreihe "Punkte aus meinen Kundenabos zum 5./10./20./25." startete bei
einer Basis von 0 und addierte nur die noch offenen Ausfuehrungen. Damit standen
die bereits vergangenen Termine auf null, obwohl im Monat laengst Lieferungen
gebucht waren, und die spaeteren Termine zeigten nur den Rest statt des
Monatsstands.
Berater 454 im September: vorher 0 / 0 / 131 / 319, jetzt 457 / 457 / 588 / 776.
Betrifft im September 86 Berater mit zusammen 15.936 Punkten — reine Anzeige,
gebucht und abgerechnet wurde korrekt.
Die Basis ist jetzt die im Monat aus den eigenen Kundenabos abgerechnete
Punktzahl, ueber dieselbe Abgrenzung wie die Backoffice-Statistik
(SalesVolumePoints::customerAboPoints(), Stornos vorzeichengetreu). Damit liest
sich die Kachel wie die Prognose-Kachel auf dem Dashboard: Stand heute plus das,
was bis zum Termin noch hereinkommt. Hinweistext in allen vier Sprachen
nachgezogen.
Nicht aus dem Audit-Paket: Die Null-Basis stammt aus der K9-Umsetzung vom 07.09.,
nicht aus der A13-Korrektur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A01 — Abo-Regeln waren ueber das Aktiv-Feld umgehbar. Das
Selbstbedienungsformular hat gar keinen Aktiv-Schalter, es spiegelt den Zustand
nur in einem versteckten Feld zurueck; `updateStatus()` hat den Wert trotzdem
uebernommen. Wer den Request manipulierte, konnte sein eigenes Abo dauerhaft
deaktivieren und so die Mindestlaufzeit umgehen, am Pausenpfad mit seinen
Sperren vorbei. Das Feld wirkt jetzt nur noch im Admin-Pfad.
A16 — Statuswechsel wurden nur aus dem Formular protokolliert. Recovery-Zahlung,
Retry, externe Erledigung und die DSGVO-Anonymisierung schreiben `active` direkt
auf das Modell. Das ist heikel, weil die historische Bestandsrueckrechnung ein
Abo verwirft, solange eine protokollierte Deaktivierung nicht durch eine
spaetere protokollierte Reaktivierung aufgehoben ist — ein stiller
`active = true` nimmt es dauerhaft aus allen Folgemonaten.
Der Eintrag liegt deshalb jetzt am Model (`UserAbo::logStatusTransition()`) und
nicht in den einzelnen Diensten: so kann ihn ein kuenftiger Schreibpfad nicht
vergessen, und er entsteht in derselben Transaktion wie die Statusaenderung.
Nebeneffekt: Die DSGVO-Anonymisierung setzt `active = false` ohne `cancel_date`
— bisher fiel ein so abgeschaltetes Abo rueckwirkend aus allen vergangenen
Monaten, jetzt bleibt die Historie korrekt.
A13 — Ein heute faelliges Abo galt ab Mitternacht als erledigt, der
Ausfuehrungsjob laeuft aber erst um 04:00. In diesem Fenster fehlten die Punkte
in der Prognose UND im Ist. Der neue Scope `UserAbo::scopeOpenExecutionUntil()`
zaehlt den heutigen Liefertag mit, solange keine Ausfuehrung angelegt ist —
genau diese Bedingung verhindert die Doppelzaehlung. Alle drei Prognosen nutzen
jetzt denselben Scope statt je einer eigenen Kopie der Datumsregel.
A11 — Der Namens-Tiebreak der Top 3 lief nach dem Abschneiden auf 25
Kandidaten, bei Gleichstand entschied also die Reihenfolge der Abfrage. Die
Auswahlgrenze wird jetzt um alle Gleichstaende an dieser Grenze erweitert, und
Kandidaten ohne Account fallen vorher weg statt die Plaetze zu blockieren. Als
letzte Regel entscheidet die user_id, damit eine gecachte Rangliste
reproduzierbar bleibt.
A14 — Sortiert wurde ueber den angezeigten Zellentext. Unter `en` liefert die
Anwendung `Y-m-d` und `1,000.00`, damit ergab jedes Datum 0 und `1,000.00` wurde
zur 1. Die Zellen tragen jetzt `data-sort-value` mit dem unformatierten Wert;
ohne Attribut gilt weiter der Textweg. Dabei mitgenommen: Umlaute landeten hinter
dem Z (jetzt localeCompare), leere Werte lagen als 0 zwischen den negativen
Stornowerten, und `next_date` wurde doppelt formatiert.
Ausserdem A08 (Prognose auch ohne erreichte Qualifikationsstufe), A09
(Summenzeile im Neupartner-CSV), A10 (Lieferungszahl vorgeladen), A17
(Faelligkeitsgruppen ohne Monatsueberlauf), A18 (Formelinhalte im CSV als Text)
und A19 (fehlgeschlagene Provisionsberechnung ist als solche erkennbar statt
0,00 EUR).
Audit-Befunde A01, A08, A09, A10, A11, A13, A14, A16, A17, A18, A19.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Am ersten des Folgemonats wird der Monat abgerechnet, die Provision berechnet
und Gutschriften werden ausgestellt. Deswegen fliessen auch Stornos nicht mehr
rueckwirkend ein. Genau dieser Stand muss eingefroren werden — und zwar
einschliesslich Teamstruktur und Detaillisten, nicht nur der Kennzahlen.
Bisher hat `overlayCurrentAboStock()` beim Lesen eines gespeicherten Monats die
gesamte Kundenabo-Topliste live neu gerechnet, also auch abgerechnete
Monatswerte. Und die Detaillisten hinter den Kacheln lasen Monatsbuchungen und
Teamzuordnung grundsaetzlich neu — nach einem Sponsorwechsel oder einer
Loeschung konnte derselbe historische Klick andere Ergebnisse liefern als die
Kachel darueber.
Das Overlay entfaellt; der Snapshot wird unveraendert zurueckgegeben. Die
Detaillisten lesen die eingefrorene Mitgliedermenge aus dem Snapshot
(`user_ids` lag dort bereits, wurde nur nie benutzt) und die eingefrorenen
Abo-Ids aus dem neuen Feld `abo_ids`. Payload-Version 2.
Grenze, die bleibt: Ein Rebuild vergangener Monate friert die Struktur des
AUSFUEHRUNGSTAGS ein, nicht die des damaligen Monatsendes — frueheres
Sponsoring wird nirgends historisiert. Der `--force`-Lauf ist deshalb nur
richtig, wenn man bewusst einen einheitlichen Stand herstellt. Das ist hier der
Fall (Schemawechsel des Payloads), fuer den laufenden Betrieb genuegt der
naechtliche Lauf ohne `--force`.
Loest die August-Entscheidung F7 ("Abo-Bestaende tagesaktuell") ab. Zwei Tests
hielten die alte Regel fest und beschreiben jetzt die neue.
Mit drin, weil dieselben Dateien betroffen sind:
- Negative Monatssalden verschwanden aus dem Drilldown (`HAVING > 0`), obwohl
ihre Betraege in der Uebersicht steckten. Nur die 1000-Punkte-Liste behaelt
ihren Schwellenfilter (A03).
- Zusatzpunkte einer stornierten Bestellung werden anteilig verrechnet statt
voll abgezogen (A06).
- Unformatierte Sortierwerte fuer die Detailliste (A14, siehe Folgecommit).
Audit-Befunde A03, A04, A06, A12.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ein Storno einer Shop-Bestellung wurde als Eigenpunkt gebucht und liess damit
die Shop-Provisionsbasis ungemindert. Betroffen sind 7 Faelle aus April bis
August 2026 (178,82 EUR Differenz in der Provisionsbasis, gegen user_businesses
geprueft).
Ursache: `status_turnover` beschreibt nur, wohin der UMSATZ gehoert. Die Punkte
folgen einer anderen Regel — eine Gutschrift (Status 4) bucht ihre Punkte immer
auf KP/TP, auch wenn ihr Umsatz dem Shop zugeordnet ist. Die Gegenbuchung kann
diese Aufteilung ohne die Ursprungsart nicht spiegeln, deshalb haelt
`storno_origin_status` sie jetzt auf der Stornozeile fest. NULL heisst
"Altzeile ohne Angabe" und faellt auf das bisherige Verhalten zurueck —
abgeschlossene Monate aendern sich dadurch nicht.
Stornoregel (Kundenentscheidung): Ein Storno mindert nur den Monat der
Ursprungsbestellung. Ist der Monat abgeschlossen, passiert nichts mehr, denn
Abrechnung und Gutschriften stehen. Ein Minus in den neuen Monat mitzunehmen
waere nicht nachvollziehbar. Fuer den Ausnahmefall gibt es den Settings-Schalter
`storno-points-current-month`, standardmaessig aus.
Ausserdem: Die persoenliche Monatsstatistik summierte Status 0 (nicht
zugeordnet) und 3 (pending) mit, die gemeinsame Punkteauswertung nicht. Beide
nutzen jetzt dieselbe Statusliste, und ein Storno mindert das Segment seiner
Ursprungsbuchung statt pauschal in "Sonstige" zu laufen.
Die Migration fuer `status_turnover` zieht nur das Schema auf den tatsaechlichen
Stand nach (nullable statt NOT NULL) — auf Produktion faktisch wirkungslos,
aber Neuinstallationen und die Testdatenbank haben damit dieselbe Semantik.
Audit-Befunde A02, A06, A15, R01-R03.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>