Commit graph

183 commits

Author SHA1 Message Date
03416b0afe 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>
2026-09-18 08:59:45 +00:00
b7e8bbd951 Statistik: Neupartner zaehlen im Monat der Zahlung, nicht der Registrierung
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>
2026-09-16 14:48:01 +00:00
cf0311afc1 Abo (A1+A2): Liefertag frei waehlbar 1-31, Aenderung nur nach vorn, Sperre erst am Ausfuehrungstag
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>
2026-09-16 11:28:55 +00:00
d6efe56cd4 Abo (A3): Status und Grund eines Abos sichtbar machen
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>
2026-09-16 10:24:18 +00:00
e8057f9f7b Abo (A4): Abschnitt "Abo pausieren oder kuendigen" mit Kuendigungs-Hinweis
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>
2026-09-16 10:14:51 +00:00
74aefa9640 Team-Statistik: Baum-Abzeichen als gefuelltes Badge (Outline-Variante war weiss auf weiss)
badge-outline-primary ist nur im Corporate-Theme definiert; im aktiven Theme fiel der Text
auf color:#fff zurueck. Solides badge-primary wie in den uebrigen Ansichten.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 15:44:07 +00:00
44637d06a7 Team-Statistik: Neu-Badge-Link kannte $selectedTree nicht (ViewException auf Produktion)
Die Closure fuer den Link des gruenen "+N"-Badges band die neue Variable nicht per use ein;
sobald eine Linie neue Abos zaehlte, brach die Uebersicht mit "Undefined variable $selectedTree" ab.
Regressionstest rendert die Uebersicht mit einem neuen Teamabo und prueft den Badge-Link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 15:40:22 +00:00
96545364f1 Team-Statistik = Team-Struktur (Provisionsbaum, Umschalter) und Laufstand-Fix in user_sales_volumes
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>
2026-09-14 15:31:32 +00:00
685f7a90b9 Kundenabo-Seite: Vorschau taggenau, Liste mit den tatsaechlich gebuchten Punkten
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>
2026-09-11 08:19:35 +00:00
cb5664a483 Kundenabo-Vorschau: bereits abgerechnete Punkte fehlten in der Basis
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>
2026-09-11 07:36:13 +00:00
ff0860fb81 Doku: Deploy-Notiz zum Statistik-Paket vom 10.09.2026
Haelt fest, was live gegangen ist, welche Daten der Snapshot-Neuaufbau
tatsaechlich angefasst hat (nur backoffice_statistics_snapshots, 8 Monate,
7 VIP-Konten) und welche nicht (Provisionen, Gutschriften, Marketingplan).

Wichtig fuer spaetere Rueckfragen: Die historischen Monatswerte koennen sich
verschoben haben, weil der neue Snapshot die Struktur des Ausfuehrungstags
einfriert und Shop-Stornos jetzt zu den Kundenpunkten zaehlen. Ein
Vorher/Nachher-Vergleich wurde nicht erstellt, die alten Snapshot-Staende sind
ueberschrieben.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 11:43:15 +00:00
233cd01bb5 Doku: Statistik-Audit 09.09., Bewertung und Umsetzungsstand
Der Ordner dev/2026-07-22 ist ueber Monate gewachsen; ohne Einstieg findet man
darin nichts mehr. Deshalb ein README als Wegweiser, mit dem Hinweis, dass bei
Widerspruechen immer die juengere Entscheidung gilt (Beispiel: F7 wurde am 10.09.
abgeloest).

Die Bewertung traegt oben einen Stand-Block mit erledigten und offenen Befunden
samt Sprungmarken. Ueberholte Aussagen der ersten Fassung sind markiert statt
geloescht, damit der Weg nachvollziehbar bleibt.

Der Monatsabschluss steht als eigenes Dokument, weil es eine dauerhafte
Fachregel ist und keine Audit-Antwort: was eingefroren wird, was die Regel nicht
leisten kann, und die Betriebsbefehle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 11:19:50 +00:00
5a38f4ac3e Statistik: uebrige Audit-Korrekturen (Abo-Regeln, Prognose, Sortierung)
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>
2026-09-10 11:19:38 +00:00
92344f7080 Statistik: abgeschlossene Monate werden vollstaendig eingefroren
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>
2026-09-10 11:19:06 +00:00
1900860c74 Statistik: Stornos mindern die richtige Punktekategorie
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>
2026-09-10 11:18:40 +00:00
75c2790bd2 Warenkorb: nicht-numerische Mengen brechen die Gewichtsberechnung nicht mehr
Yard::weight() multiplizierte Gewicht und Menge ohne Cast. Beide Werte stammen
aus Session- bzw. Request-Daten; ein fuehrend-numerischer String wie "2,5" loeste
unter PHP 8 die ErrorException "A non-numeric value encountered" aus. Danach war
die Position dauerhaft defekt, da reCalculateShippingPrice() bei jedem Aufruf
erneut fehlschlug.

Cart::update() setzt die Menge ohne Validierung (anders als setQuantity()).
Eintrittspunkte waren das quantity-Feld im Shop-POST sowie das Textfeld fuer die
Menge im Backoffice-Warenkorb.

- Yard: neue cartItemWeight() mit explizitem int-Cast fuer Gewicht und Menge
- CardController: nicht-numerische Mengen werden uebersprungen, Schutz gegen ein
  quantity-Feld, das kein Array ist
- OrderController: nutzt das bereits vorhandene $qty (int) statt $data['qty'] roh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 07:16:30 +00:00
68d230d4e5 Stripe: Kundendaten, Rechnungsnummer, manueller Zahlungseingang
Rueckmeldung der Buchhaltung und des Stripe-Calls (dev/2026-08-26-stripe/):
Stripe kannte bisher nur die E-Mail des Kunden, Zahlungen waren im Dashboard
weder ueber die Bestell- noch ueber die Rechnungsnummer auffindbar, und ein
Zahlungseingang mit falschem Verwendungszweck zwang die Buchhaltung dazu, die
Rechnung von Hand zu schreiben.

- SE1: Der Checkout haengt einen Stripe-Customer mit Name, E-Mail und
  Rechnungsanschrift an die Session (customer statt customer_email); die ID
  liegt an shopping_users und wird wiederverwendet. Namensformat "Vorname
  Nachname (Firma)", reine Firmenkunden unter der Firma. Faellt die
  Customer-Anlage aus, laeuft der Checkout auf dem bisherigen Weg weiter.
  DSGVO-Loeschungen entfernen jetzt auch das Customer-Objekt; die Charges
  bleiben als Buchungsbeleg bestehen.
- SE2: StripeMetadataBuilder als einzige Quelle fuer Metadaten und die
  description (Bestellnummer, Referenz, Kundenname) - damit greift die
  Dashboard-Suche, die bisher nichts Lesbares zu fassen bekam.
- SE3: Die Rechnungsnummer entsteht erst nach der Zahlung und wird daher per
  Job am PaymentIntent nachgetragen (Vorab-Reservierung wuerde Luecken im
  Nummernkreis erzeugen). stripe:backfill-invoice-metadata traegt sie
  rueckwirkend ab dem Go-Live 2026-07-10 nach.
- SE4: Neuer Admin-Button "Zahlungseingang erfassen" an jeder unbezahlten
  Bestellung - bisher gab es ihn nur bei Rechnungskauf, und er setzte nur
  paid, ohne Rechnung, Punkte, Abo-Aktivierung oder Mail. Der neue Weg laeuft
  durch paymentStatusPaidAction, verlangt den Verwendungszweck als Beleg und
  laesst einen abweichenden Betrag nur mit ausdruecklicher Bestaetigung zu.
- S6.4 (aus dem Juli-Plan vorgezogen): stripe:sync-payment-status gleicht
  offene Zahlungen aktiv gegen die API ab. Aufgefallen beim Testlauf - ein nie
  zugestellter Webhook war bisher nicht reparierbar, das Geld lag bei Stripe
  waehrend die Bestellung auf offen stand. Laeuft stuendlich im Scheduler.
- 110 Tests (Pest) fuer alle Pfade, Stripe-Client durchgehend gemockt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 09:04:28 +00:00
30fb031d12 Mitgliedschaft: Erinnerungen und eigenes Verlaengerungs-Produkt
Die Erinnerungsmails vor Ablauf der Mitgliedschaft beschrieben noch den
abgeschafften SEPA-Einzug und verlinkten auf die Hauptdomain, wo
/user/membership nicht existiert. Das Produkt fuer die Verlaengerung war
zudem dasselbe wie bei der Registrierung und sprach deshalb vom
Neuabschluss.

- Erinnerungen: nur noch Stufen 31/33/34/35, SEPA-Stufen 32/36/37 samt
  :price und :pay_date entfallen; Button verlinkt auf my.mivita.care;
  fehlt ein Text zum Status, wird die Mail geloggt statt roh versendet.
- Alle Erinnerungstexte (de/en/es/fr) erklaeren jetzt, dass ohne Zahlung
  der Account pausiert und offene Provisionen ausgezahlt werden.
- Eigenes Produkt fuer die Verlaengerung (identifier show_renewal) als
  Kopie des Mitgliedschafts-Produkts inkl. Preisen, Kategorien und
  Uebersetzungen fuer en/es/fr; die Mitgliedschaftsseite faellt ohne
  dieses Produkt auf das bisherige zurueck.
- Mitgliedschaftsseite auf ein Paket reduziert: Upgrade-, Wechsel- und
  Abo-Optionen entfernt, Texte auf die manuelle Verlaengerung umgestellt.
- Alte Cron-Weboberflaeche entfernt, der Versand laeuft ueber den
  Scheduler (payments:check-accounts).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:24:32 +00:00
0bdb6eabc6 Slim: sanitizeFileName kürzt hinten statt vorne und behält die Dateiendung
- Lange Namen werden am Ende gekürzt (vorher substr(-33): "Bildschirmfoto…"
  wurde zu "ldschirmfoto…", der Namensanfang fiel weg)
- Whitespace inkl. geschütztem Leerzeichen (macOS-Screenshots) wird zu "-",
  damit der Dateiname ohne Encoding in URLs funktioniert
- Fallback "datei", wenn nach der Bereinigung nichts Verwertbares übrig bleibt
- 5 Unit-Tests für Kürzung, Whitespace, Sonderzeichen und Fallback

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 11:11:01 +00:00
8afaf9d4e2 Kundenrueckmeldung 25.08.: Formulierung zur Prognose-Gegenpruefung praezisiert
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:48:54 +00:00
94684f7ffd Firstlines-Kachel: Beschriftung praezisiert (Berater statt aktiv)
Auf der Firstlines-Seite koennen Zeilen ein 'Account abgelaufen'-Badge tragen und
trotzdem in die gezaehlte Menge fallen - 'aktiv' war dafuer das falsche Wort. Die
Kopfzeile heisst jetzt ':active Berater - :total insgesamt' (de/en/es/fr), passend
zur 'Berater'-Spalte der Statistik.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:48:28 +00:00
8dc0361fec Firstlines: Seite und Dashboard-Kachel zaehlen wieder dieselbe Menge
Beim Formulieren der Kundenrueckmeldung aufgefallen: der vorige Commit hat die
Inkonsistenz an einer Stelle behoben und an einer anderen wieder eingebaut. Die
Kachel zaehlt ueber isActiveConsultant() (Mitgliedsebene + gesetztes
Abrechnungskonto), die Kopfzeile der Firstlines-Seite zaehlte aber ueber
isActiveAccount() (Konto in der Zukunft). Bei abgelaufenen Jahresgebuehren nannten
Kachel und Seite damit unterschiedliche Zahlen - genau das, was der Fix vermeiden
sollte.

firstlines() bekommt den BackofficeDashboardService injiziert und nutzt dieselbe
Regel. Test um beide Randfaelle erweitert (abgelaufenes vs. gar kein
Abrechnungskonto), damit die Doppel-Definition nicht zurueckkommt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 10:47:50 +00:00
cee4a2e0eb Statistiken: Review-Fixes vor Livegang (Fehldarstellungen + Test-Regression)
Ergebnis des Senior-Reviews über dev/2026-07-22 (Konzept 22.07., Feedback 24.07.,
Plan 19.08.). Rechnerisch war nichts zu korrigieren: Payline-Punkte und Provision
des Prognose-Blocks (N4) stimmen exakt mit der Provisions-Engine überein
(54.718,5 P. bzw. 3.772,88 EUR gegengerechnet an User 454). Behoben sind
Darstellungs- und Konsistenzfehler, die der Beraterin als falsche Zahlen
aufgefallen wären:

- "+N neu" schrumpfte rückwirkend, der Bestand daneben nicht: newAboCount() nutzte
  weiter activeAbo(), während aboStockCount() auf das Monatsende zurückrechnet.
  Beide teilen sich jetzt aboStockQuery(); die Jahres-Wachstumstabelle ebenso.
- Level-Kachel widersprach sich ("Zum nächsten Level Diamant fehlen X" über
  "Karriere-Level: Diamant"): Text auf die Monats-Qualifikation umformuliert
  (de/en/es/fr), Überschrift "Qualifikation in diesem Monat", Karriere-Level nur
  noch bei Abweichung.
- Firstlines-Kachel zählte alle direkten Partner, "Teampartner gesamt" daneben nur
  aktive (F1): Kachel nutzt jetzt isActiveConsultant(), die Detailseite listet
  weiter alle und weist beide Zahlen aus.
- Neupartnerzahlen vergangener Monate sanken auf dem Home-Dashboard, sobald
  Jahresgebühren ausliefen: neue User::newPartnerReferenceDate() (laufender Monat =
  heute, abgeschlossener Monat = Monatsende). Dabei die drei Inline-Kopien der
  Neupartner-Regel auf scopeNewPartnerIn()/isNewPartnerIn() zusammengeführt.
- Top-3-Karte ließ den Betrachter aus, obwohl "ich gehört in die Liste" in der
  Juli-Runde für alle anderen Ranglisten umgesetzt wurde: zählt jetzt mit, inkl.
  is_self und "ich"-Badge.
- Neues Profilfoto zeigte bis zu 6 h ein totes Bild: die URL steckt nicht mehr im
  Cache, sondern entsteht beim Lesen (withProfileImages()).
- Test-Regression seit der Juli-Runde: Unit/Services/BackofficeDashboardServiceTest
  fiel über die fehlende Spalte status_points (kam mit nonPaylinePoints() dazu).
- Kleinigkeiten: ProfileImageController gab die komplette Exception als
  Flash-Message aus (jetzt Log + Sprachkey), BackofficeDrilldownService formatierte
  Datumsangaben hart auf d.m.Y statt über Util::formatDateDB().

Tests: neuer HomeMonthlyStatsTest (buildMonthlyStats war ungetestet) plus Tests für
Stichtags-Zählung und Foto-Cache. Fünf Tests kodierten das alte Verhalten
(Betrachter zählt nicht mit, abgelaufener Account zählt nie) und wurden auf die
neue Semantik umgestellt, jeweils mit Gegenprobe. Suite: 861 grün (vorher 855),
89 rot — ausschließlich die in docs/2026-06-11-test-suite-vorbestehende-fehler.md
dokumentierten Altlasten.

Offen und bewusst nicht enthalten:
- Deploy-Schritt: backoffice:store-statistics-snapshots --force (vorher dumpen).
  Kein vorhandener Snapshot enthält recruiter_ranking, Mai/Juni zusätzlich weder
  non_payline_points noch die Führungskräfte-"gesamt"-Spalten; der Cron überspringt
  vorhandene Snapshots ohne --force. Achtung: die Neuberechnung ist nicht
  verlustfrei.
- TreeHtmlRendererTest/TreeCalcBotIntegrationTest: die Abo-Badges machten den
  Renderer datenbankabhängig; die Dateien waren schon vorher rot und gehören in die
  separate Test-Aufräumaufgabe.
- deleteImage bleibt ein GET-Route (gleiches Muster wie das Shop-Bild).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 09:41:53 +00:00
f6faad9ae7 Abo: extern-erledigt für gelöschte Abos + Lieferhistorie-Umzug (Ticket Kunz Mai-Punkte)
- AboExternalCompletionService: Bestellungen gelöschter/ersetzter Abos können
  nachträglich extern erledigt werden (Punkte + Status 8 + Versandstatus);
  das gelöschte Abo wird dabei nicht reaktiviert
- IncentiveTracker::trackSalesVolume: Abo-Zuordnung inkl. gelöschter Abos
  (withTrashed) - nachgebuchte Umsätze bereits getrackter Abos zählen weiter
- Neue Wartungs-Commands: abo:extern-done {shopping_order_id} und
  abo:move-orders {from_abo} {to_abo} (--dry-run, Guards: gleicher Kunde)
- Latent datumsabhängigen Alt-Test in AboExternalCompletionTest repariert
  (Buchung nutzt date(), nicht Carbon::setTestNow)

Fall Kunz (dev/2026-08-12-intensiv-tickets): Dannenhauers gelieferte, aber am
17.06. beim Nachtragen vergessene Mai-Order 53274 → abo:extern-done (+54 P);
Bockhackers Lieferhistorie von gelöschtem Abo 197 → 496 umgehängt (Zähler 4).
Bockhackers 24 P existieren bereits (Juni-Buchung der Mai-Lieferung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 12:06:29 +00:00
867dc21b57 Statistiken: Kundenfeedback 2026-08-19 komplett (N1-N8) + Review-Fixes + VP-/Incentive-Repair gehärtet
Statistik-Änderungen 2026-08-19 (dev/2026-07-22/entwicklungsplan-2026-08-19.md, alle 8 Pakete gemäß beantworteten F-Fragen):
- N1 Monatsstatistik: Team-Cluster (Firstlines klickbar -> neue Seite user_team_firstlines, Teampartner gesamt/Payline [nur aktive], Abos Payline mit +N) + Level-Kachel (Quali-Stufe, fehlende Payline-/persönliche Punkte, Karriere-Level); LiveCommissionService-Snapshot v3 reicht Level-/Quali-Daten durch
- N2 Abo-Kacheln auf Home monatsbezogen (aboStockCount-Rückrechnung über cancel/deleted/Status-Historie; VIP-Statistik bleibt tagesaktuell)
- N3 VIP-Statistik 'Recruiter des Monats' (Neupartner-Definition wie KPI, rekursives Ketten-Volumen als Gesamtpunkte inkl. Starterpaket) + Drilldown/CSV-Spalte 'Punkte gesamt'
- N4 Prognose-Block 'Level & Provision laut Prognose (Stand 25.)' mit unverbindlich-Kennzeichnung (ebenengenaue offene Abo-Punkte + TP-Laufstände)
- N5 Checkout-Herkunft: Dropdown raus, Pflicht-Freitext (lokalisierte Fehlermeldung)
- N6 Berater-Profilfoto (user_accounts.profile_image, Einwilligung als Sprachkey-Platzhalter, DSGVO-Löschung) + teaminterne Top-3-Karte (TopResultsService, Scope-Schalter)
- N7 USt-Optionen: 'nicht umsatzsteuerpflichtig' zuerst + '§ 19 UStG'-Textfix
- N8 Jahresgebühr-Nachfassliste je Berater (AnnualFeeDueService, überfällig/1-3 Monate, Sidenav)

Kontroll-Review (8 Perspektiven) angewendet: next-Level-Fallback im Steady-State (resolveNextLevel — sonst 'Höchstes Level erreicht' für Normalfälle), _calculated-Punktebasis bei mehr Paylines, keine Wiederbelebung deaktivierter Abos in der Monatsrückrechnung, Payline-Abo-Kachel monatskonsistent, Top-3-Nachrücker; Performance: BackofficeDashboardService als Singleton mit lineBuckets-Memo (+flushLineBucketsMemo), level_forecast 6h-Cache; Reuse: User-Scopes firstlinesOf/newPartnerIn, SalesVolumePoints::totalPointsByUser, Slim::storeCroppedImage (3 Upload-Kopien vereinigt), activeAbo-Scope statt Inline-Prädikate, Form::open-Konvention, Return-Types

Repair-Härtung (Kundenentscheide): VP-Nachtrepair dedupliziert über shopping_order_id (Live-Pfad schreibt sie jetzt mit), zählende Alt-Zeilen-Erkennung, Vormonats-Scan für Monatswechsel, Firmenaccount-Buchungen (config mivita.company_user_id) werden automatisch auf den Berater umgebucht (in place bzw. per Gegenbuchung bei geschlossenem Monat; Echtfall Anna Schall Order 58797 -> Christina Schuder), Berater-zu-Berater nur Warnung; Incentive-Repair: m_sponsor=NULL löscht nicht mehr, FK-Repair vor Stale-Bereinigung, gebatchte Lookups, zentrales Incentive::qualificationWindow(); .env aus dem Git-Tracking entfernt (Pflege per SFTP)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:44:44 +00:00
841d87e161 Incentive: Ticket-Fixes 2026-08-12 + Team-Struktur nach active_date sortiert
Incentive (Montenegro 2026):
- Live-Tracker: whereDate() für Zeitraum-Vergleiche – Bestellungen am letzten
  Tag des Qualifikationszeitraums gingen verloren (DATE-Spalte vs. Datetime)
- Repair/Rebuild: Neupartner-Definition auf Starterpaket-Bestelldatum im
  Zeitraum umgestellt (statt users.created_at) – Bestandskunden-Upgrades
  zählen jetzt wie im Live-Tracking (Fall Dino Portaro → Steffi Karrer)
- Neu: removeStaleSponsorTrackings() – Partner-Trackings wandern nach
  Sponsorwechsel zum aktuellen Sponsor (Fall Anna Schall lag beim Firmenkonto)
- incentive:calculate: Sponsorwechsel-Bereinigung eingebunden + Statistik

Team-Struktur (/user/team/structure):
- Downline sortiert nach active_date (Partner-Aktivierung) statt users.id:
  zentral in TreeHtmlRenderer::sortByActivation() (wirkt sofort, auch für
  gespeicherte Monats-Snapshots) + orderBy in beiden Downline-Queries

Doku: dev/2026-08-12-intensiv-tickets/README.md (4 Kundentickets analysiert,
Lücke durch verspätete Zeitraum-Verlängerung hatte sich per Nacht-Cron geheilt)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 10:28:06 +00:00
ac3bd6e8ea Statistiken/Abo: Kundenfeedback 2026-07-24 komplett umgesetzt + Steuern: DATEV-Fixes 08+09/2025 (Sammel-Commit Branch-WIP)
Feedback-Umsetzung (dev/2026-07-22/feedback-analyse-2026-07-24.md, alle 10 Punkte):
- VIP-Statistik: eigener Berater ('ich', Linie 0) in Neue-Kundenabos- und beiden Top-10-Listen; Top-10-Umsatz sortiert nach abgerechneten Ist-Punkten + Abo-Anzahl-Spalte; Fußnote nicht-payline-wirksame Punkte (SalesVolumePoints::nonPaylinePoints)
- Führungskräfte-Liste: beide Sichten 'direkt' (an nächster FK abgeschnitten, additiv) und 'gesamt' (komplette Downline), Gesamt-Spalte nach Kundenzählweise
- Team-Abos: je Ebene 'abgerechnet (Monat)' und 'bis zum 25. erwartet'; Payline-Karte mit Teamabo-/Kundenabo-Zählern
- Incentive-Live-Ranking: Qualifizierte zuerst nummeriert (fixt auch isWinner rank<=max_winners)
- Porto: Netto-/Brutto-Kennzeichnung (Berater netto, Endkunden inkl. MwSt., portal_view-Flag; auch CRM-/Shop-Warenkorb + Checkout), Yard::shipping_tax-Fix
- Struktur-Ansicht: Abo-Badges 'A' (eigenes Abo) und '(n)' (Kundenabos) je Teampartner, gebatcht
- Abo-Löschung: Softdelete setzt active=0 (UserAbo deleting-Hook) + Bereinigungs-Migration für gelöschte-aber-aktive Abos
- Doku: technische Analyse + Kundinnen-Feedback-Dokument unter dev/2026-07-22/
- Tests: IncentiveRankingTest, TeamStructureAboBadgesTest, YardShippingTaxTest (neu); BackofficeStatisticsListsTest, TeamAboOverviewTest, PaylineForecastServiceTest, AboAdminDeleteTest, AboOneTimeViewTest erweitert

Außerdem enthalten (Branch-WIP vor dem Feedback): DATEV-Abgleich-Fixes 08+09/2025, USt-ID-Live-Validierung sowie restliche unkommittete Stände (u. a. Abo-Pause-Sprachkeys).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:56:02 +00:00
657d4bd5b7 Abo: Fix Checkout-Rücksprung bei Einmalprodukten in der Bestellstrecke
Die Submit-Validierung checkSendYardForPayment() prüfte jede Cart-Zeile
gegen das Abo-Sortiment (12/13) – Einmal-Zeilen (show_on 3) fielen durch
und der Checkout-Klick sprang per back() kommentarlos auf die
Bestellseite zurück. Einmal-Zeilen werden jetzt gegen das
Einmal-Sortiment validiert (manipulierte Zeilen weiterhin abgewiesen).

Zusätzlich zeigt list.blade.php jetzt alle Validierungsfehler an
(vorher nur Comp-Produkt-Fehler → Fehler blieb unsichtbar).

Tests: 2 neue Regressionstests in AboCreateOneTimeCartTest (19 grün).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 13:02:43 +00:00
cdb121fde0 Abo: Einmalprodukte bereits bei der Abo-Anlage + es/fr-Sprachsync
Einmalprodukte (Normalsortiment, show_on 3) können jetzt schon beim
Abo-Abschluss hinzugefügt werden – im Portal-Wizard (Abschnitt im
Upgrade-Schritt) und in allen drei Sales-Center-Abo-Strecken (eigene
Produktliste). Variante A: Zeilen laufen als Cart-Option abo_addon mit
durch den Checkout, werden sofort bezahlt und mit der Erstlieferung
verschickt (Rechnung/Lieferschein gruppieren automatisch).

- Checkout persistiert is_abo_addon (CheckoutRepository, PaymentHelper)
- createAboItems/ensureUserAboItemsFromLatestOrder überspringen Addons
  (sonst würden Einmalprodukte dauerhaft wiederkehren); amount ohne
  Addon-Anteil; aboHasBaseProduct ignoriert Addon-Zeilen
- CreateAboReplacementOrder propagiert is_abo_addon in Ersatzbestellungen
- iq-shopping-cart.js: zweite Datatable + abo_addon-Flag-Weitergabe
- i18n: neue abo.onetime_create_*-Keys (de/en/es/fr); zusätzlich es/fr
  komplett synchronisiert (abo-Kartenblock, navigation, payment,
  weborder inkl. Entfernung PAYONE-Altlasten, withdrawal.php neu)
- Tests: AboCreateOneTimeItemsTest, AboCreateOneTimeCartTest,
  AboPortalCreateOneTimeTest (17 neue, 122 Tests gesamt grün)

Plan: dev/2026-07-21-abo-einmalprodukte-anlage/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:36:40 +00:00
1e7ae92d0c Stornorechnung: Fix überdeckte Artikelzeile + Regenerate-Command
Der ACHTUNG-Hinweiskasten hat wegen fixer 15mm-Höhe des Titel-Blocks
den Tabellenkopf und die erste Artikelzeile überdeckt (z.B. Artikel
5010400 auf den Stornos zu Bestellung 56965/56966). Titel-Block wächst
jetzt mit dem Inhalt.

Neuer Command sales:regenerate-cancellation-pdf erzeugt die PDFs
bestehender Stornorechnungen neu (gleiche Nummer/Pfade, keine
Punktebuchung, keine Mails) – per Rechnungs-ID oder mit --order per
Bestell-ID.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 14:19:28 +00:00
8e09aaefe8 Stripe-Integration (S7.4): Payment-Dashboard für Stripe
- Provider-Status/Uptime: Stripe-Kachel aktiviert; payment:check-uptime prüft
  zusätzlich api.stripe.com/v1/ (401 ohne Key = erreichbar) inkl. Auto-Incident
- Zahlungen: Anbieter-Filter, Provider-Badges, Umsatz je Provider,
  Stripe-Dashboard-Deep-Links (ShoppingPayment/PaymentTransaction::
  stripeDashboardUrl, pi_-Guard, testmodus-bewusst)
- Rohe Transaktionen: Anbieter-Filter/-Spalte, Erfolgsraten je Anbieter;
  Bugfix: Stringable-Vergleich ließ den txaction-Filter immer mit leerem Wert
  greifen — Liste war dauerhaft leer
- Neue Kachel „Abos je Anbieter" (Index + GF-Ansicht) mit Fehlerzählern und
  Stripe-Anteil-Fortschrittsbalken (Auslaufmodell PAYONE → Stripe ablesbar)
- Incident-Kachel weist Stripe/PAYONE getrennt aus; Log-Viewer mit
  Kanal-Umschalter stripe/payone
- Plan-Doku: S7.4-Umsetzungsstatus, Abo-Go-Live 2026-07-10 vermerkt

Tests: PaymentDashboardProviderTest (9 neu), Dashboard-Suite 49 grün.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:57:50 +00:00
ce4750fd1a Stripe-Integration (S4, S5, S7.6): Abos, Zahlungsart ändern, Zahlarten-Kommunikation
Phase S4 – Abos über Stripe (E2E komplett im Testmodus):
- Abo-/Recovery-Checkout: customer_creation=always + setup_future_usage=off_session
- StripePaymentService::chargeAbo (off-session PaymentIntent, Idempotency-Key),
  StripeGateway, Provider-Dispatch im Abbuchungs-Cron (PAYONE-Pfad unverändert)
- Webhook: Zahlungsmittel-Capture vor Paid-Pfad, 503 payment_not_yet_visible bei
  Cron-Transaktions-Race; Command stripe:process-pending-events (Cron-Ende + 10-min
  Scheduler) verarbeitet liegengebliebene Events nach
- createNewAbo/advanceAboAfterRecoveryPayment Stripe-aware inkl. Race-Absicherung;
  Recovery-Orders nutzen advanceAboAfterRecoveryPayment (latenter PAYONE-Duplikat-Bug)
- Migrationen: Stripe-Abo-Spalten auf shopping_payments; clearingtype varchar(3)→(20)
  inkl. Reparatur der zu 'str' gekürzten Live-Zeilen

Phase S5 – Zahlungsart ändern (Scope: jede Zahlart, nicht nur Kreditkarte):
- Stripe Checkout mode=setup ohne Abbuchung; PAYONE-Abos ziehen beim Wechsel auf
  Stripe um (Auslaufmodell §7.2)
- AboPaymentMethodService: beginStripeChange/finalize/applyStripePaymentMethod
  (idempotent, detacht alte PM, card_changes-Log); Webhook setup_intent.succeeded
  als Sicherheitsnetz; Einstiege Portal/CRM/Berater-Kunden-Link flow-abhängig
- Views/Mails flow-abhängig („Zahlungsweise" statt „Kreditkarte")

Phase S7.6 – Zahlarten-Kommunikation:
- Einheitliche Zahlarten-Logos (SVG) + wiederverwendbares Partial payment_logos;
  Checkout/Footer zeigen Kern-Zahlarten + „···"-Icon, /zahlungsarten komplett neu
  mit allen aktiven Stripe-Zahlarten (gegen Account-Konfiguration verifiziert)
  und Vorkasse (PAYONE); Sofort/SEPA entfernt

Tests: StripeAboTest (15), StripePaymentMethodChangeTest (10),
PaymentMethodsCommunicationTest (28), PaymentProviderManagerTest angepasst.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 14:45:17 +00:00
6f21845607 Stripe-Integration (S0–S3): Einmalzahlungen über Stripe Checkout + Notfall-Go-Live
Wegen PAYONE-Ausfall vorgezogen live geschaltet: alle Einmalzahlungen
(Shop + Salescenter) laufen über Stripe Hosted Checkout, Abos sind bis
Phase S4 eingefroren.

- Provider-Abstraktion: PaymentProvider-Enum, PaymentGatewayInterface,
  PaymentProviderManager (Settings-Schalter, provider-treu je Bestand),
  PayoneGateway als unveränderter Adapter
- StripePaymentService: Checkout Session (eine Gesamtposition, Metadata,
  success/cancel-Redirects), StripeClientWrapper (lazy, Dark Launch),
  StripeCustomerService, StripeTransactionRecorder (client_secret-Redaction)
- Webhook /stripe/webhook: Signaturprüfung, Idempotenz über
  stripe_webhook_events (Crash-Retry-Reprocessing), txaction-Prioritäten,
  Betrags-Guard, lockForUpdate-Paid-Pfad, genau eine Bestätigungsmail
- Zahlungsmethoden-Anzeige aus charge.succeeded (carddata inkl. Wallet,
  auftragsunabhängiges Backfill aus dem Event-Store)
- Checkout: Wartungsmodus mit Admin-Bypass, Abo-Freeze-Hinweis,
  Stripe-Option (Online-Zahlung) statt Zahlartenwahl, Vorkasse ausblendbar
- Abo-Freeze-Guards: Cron user:make_abo_order, Retry, Kartenupdate
- Admin-Settings-Karte Zahlungen (Provider, Wartung, Freeze, Vorkasse)
- Hotfix: 500 bei Vorkasse ohne Clearing-Daten in admin/sales/_detail
  und checkout-final (PAYONE-Ausfall)
- Doku dev/2026-07-08-stripe/ (Plan S0–S8, Konzept, IST-Analyse)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:56:41 +00:00
671022a28d Abo: Kreditkarte aktualisieren (CRM, Portal, Berater-Link) + Admin-Abo-Löschung
- Karten-Update ohne Abbuchung via PAYONE creditcardcheck/pseudocardpan
  (AboPaymentMethodService, CardUpdateResult/CreditCardCheckData-DTOs)
- Eingabe nur durch Abo-Inhaber: CRM- und Portal-Formular + zeitlich
  begrenzter Berater→Kunde-Link (AboCardUpdateLink + Mail)
- Änderungshistorie in user_abo_card_changes, Anzeige im Admin-Abo-Detail
- Admin: Abo löschen (destroy) inkl. Test
- Portal-Fix: myOrderCreate wirft 404 statt TypeError bei ungültiger ID
- VIP-Sichtbarkeitstests für Einmalartikel-Fenster ergänzt
- Enthält bereits die Freeze-Guards für den Stripe-Notbetrieb (Folge-Commit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:56:19 +00:00
63c36555a5 Widerruf: Gesetzliche Online-Widerrufsfunktion (Formular, Bestätigungsmail, Admin-Verwaltung)
- Öffentliches Widerrufsformular /widerruf mit Bestell-Lookup (throttled)
- Bestätigungsmail an Kunde + Weiterleitung an bestellungen@ (config app.withdrawal_mail)
- Admin-Übersicht/-Detail unter /admin/withdrawals inkl. Sidenav-Eintrag
- Widerruf-Button in Portal-Bestellansicht und Footer-Links
- Model OrderWithdrawal + Migration + Factory + Tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 15:56:02 +00:00
eac923968d Admin-Abo: Produkte trotz Mindestlaufzeit abziehen/entfernen
Die Admin-Detailseite postete Warenkorb-Aktionen mit view=me/ot, wodurch
Add-only-Modus und Change-Lock auch fuer Admins griffen (Reduzieren wurde
verworfen, Entfernen lieferte 403). Formular postet jetzt mit view=admin;
checkPermissions laesst view=admin nur noch fuer Admins zu (schliesst
zugleich die Luecke, dass unbekannte Views alle Besitzer-Pruefungen
uebersprangen).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 08:17:27 +00:00
d50be7135a Backoffice-Statistik: Neue-Kundenabos-Ranking & Führungskräfte-Abos
Zwei neue Listen unter der Team-Übersicht: Ranking der Berater nach neuen
Kundenabos im Monat (inkl. Gesamtbestand und Linie) sowie eigene und
Kundenabos aller Führungskräfte ab Stufe Vertriebsleiter mit Gesamtsumme.
Beides fließt mit in die Monats-Snapshots ein.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:24:01 +00:00
0660f4e0a0 Abo-Recovery: fehlgeschlagene Folgezahlung im Checkout nachholen
- Pay-now-Flow: bei fehlgeschlagenem Abo (Status 3) begleicht der Inhaber die
  offene Rechnung im Standard-Checkout mit frei waehlbarer, abo-faehiger Zahlart
  (nur eigenes Abo: Berater me-View + Kundenportal), payNow-Controller + Routen
- AboRecoveryCheckoutService baut die ShoppingInstance aus den Abo-Artikeln;
  neue Spalte shopping_orders.recovery_user_abo_id verknuepft Bestellung<->Abo
- Bei Erfolg: AboHelper::advanceAboAfterRecoveryPayment schreibt das Abo fort
  (Status->2, next_date) und uebernimmt die gewaehlte Zahlart als neues Token
  fuer kuenftige Abbuchungen
- Neue Pay-now-Fehlermail (AboPaymentFailedMail) ersetzt die generische
  Fehlermail im Abo-Cron; Link fuehrt auf die Abo-Detailseite (CRM bzw. Portal)
- Retry-/"extern erledigt"-Buttons nur noch fuer Admins; tote user/portal-
  Retry-Routen entfernt
- Checkout: Recovery-Hinweis statt "Abschluss des Abonnements", Lieferadresse
  vorbelegt, korrekter Zurueck-Link (Abo statt Shop)
- UserAbo::selfServiceDetailUrl zentralisiert die Abo-URL (Mail, Back-Links)
- Test-Command abo:test-recovery; Tests fuer payNow, Advance-Logik, Fehlermail

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 10:38:05 +00:00
78db952130 Doku: CLAUDE.md erweitert & Salescenter-/Arbeitsnotizen
CLAUDE.md um Multi-Tenant-Routing, Payment/PAYONE, Konventionen & Dev-Test-
Commands ergaenzt. Neue Arbeitsnotizen unter docs/salescenter/ sowie
docs/Kurzfristige Anpassungen.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:53 +00:00
84238c7694 Subdomain-Cleanup-Commands
Neue Artisan-Commands DeleteAllKasSubdomains und DeletePartnerShopSubdomains
zum Aufraeumen von KAS-/Partnershop-Subdomains, jeweils mit Feature-Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:45 +00:00
9b7665c59a Abo-Wartungs-Commands & Ausfuehrungs-Reminder aktivieren
Neue Artisan-Commands CreateAboReplacementOrder (Ersatzbestellung fuer ein
Abo anlegen) und RepairAboOrderUserAssignment (fehlerhafte User-Zuordnung
von Abo-Bestellungen korrigieren), jeweils mit Feature-Tests.
Ausserdem den abo:send-execution-reminder-Schedule (06:00) aktiviert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:39 +00:00
2b2847add1 Abo: Doppelbestellung verhindern, Payment-Link loeschen & Pending-Status
Verhindert das Anlegen eines zweiten Abos (eigenes oder Kundenabo) wenn
bereits eines initiiert ist bzw. ein offener Abo-Payment-Link existiert
(redirectIfAboAlreadyInitiated). Payment-Links koennen jetzt geloescht
werden, abgesichert ueber canDeleteInstance + Shop-Zugehoerigkeit, mit
Erfolgs-/Fehler-Flash. Kundenabo-Uebersicht zeigt den Pending-Initial-
Payment-Status (wartend / in Bearbeitung / fehlgeschlagen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:30 +00:00
42ef4dd1c6 DHL-Versand: Sendungs-Info-Button in Bestelllisten & Absenderadresse
Neuer schlanker DHL-Info-Button (getDhlShippingButton) in den Order- und
Shop-Sales-Datatables, oeffnet ein Read-only-Modal (modal_dhl_shipping_info)
mit den Tracking-Daten; dhlShipments wird dafuer eager geladen.
Absenderadresse in config/dhl.php aktualisiert, plus kleinere Fixes in
DhlShipmentController/Service/DataHelper und CreateReturnLabelJob.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:37:13 +00:00
7a620d66ae Backoffice-Statistik: Punkte-Fix (kumulative Snapshots) & Meine Statistik
Punkte/Umsatz korrekt aus per-Zeile points/total_net je status berechnen
statt SUM der kumulativen month_*-Snapshots (zaehlte massiv mehrfach).
Zentralisiert in SalesVolumePoints, genutzt von Dashboard- & Drilldown-Service.

Detail-Tabelle neu gegliedert: Eigenpunkte | Shop | Abo | Sonstige |
Kundenpunkte (=Summe) | Gesamt, mit gruener Hervorhebung der Basis-/Summenspalten.

Neue VIP-Seite "Meine Statistik" (Untermenue neben Team-Statistik):
KPI-Karten + nachvollziehbare Detailtabellen, gruppiert nach eigenen
Bestellungen, Shop-Bestellungen und Abos (PersonalStatisticsService).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 13:31:14 +00:00
9565814221 Team-Abo-Uebersicht: nur aktive Berater zaehlen
Die Linientabelle zaehlte alle Downline-User statt nur aktive Berater
(m_level + gueltiges payment_account), wodurch zu viele Berater angezeigt
wurden. Filter-Logik aus BackofficeDashboardService (isActiveConsultant,
activeTeamUserIds) wiederverwendet und in TeamAboOverviewService
(buildLineRow, teamKpis, monthlyGrowth) angewendet.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-25 13:25:44 +00:00
386851aee8 Backoffice-Statistik vereinfacht & Dashboard-Monatsstatistik konsolidiert
- Statistik-Uebersicht: Tabelle auf Linie/Berater/Umsatz(Punkte)/Teamabos/
  Teamkundenabos reduziert; Neupartner als grünes "+N"-Badge in der
  Berater-Spalte; 1000-Punkte-Shop & Neupartner als klickbare KPI-Cards
  (Augen-Icon/Liste anzeigen)
- Teamkundenabos-Drilldown pro Teampartner gruppiert (Anzahl + Volumenpunkte
  gesamt, aufklappbar je Abo), CSV-Export angepasst
- Home: fehlerhafte Monatsstatistik-Card (inkonsistente aktiv-Definition,
  kein Monatsbezug, schwere Live-Logik im Blade) mit der Abo-KPI-Card
  zusammengefuehrt; Logik nach HomeController::buildMonthlyStats() ausgelagert,
  konsistent ueber TeamAboOverviewService/BackofficeDashboardService
- Team-Payline fuer laufenden Monat live berechnet und 6h gecacht (Snapshot)
- Kacheln in Cluster (Umsatz/Neupartner/Abos) gruppiert
- Services: teamKpis() um *_new ergaenzt, personalAboKpis() & newPartnersCount() neu
- i18n (de/en/es/fr), Tests ergaenzt/aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 13:00:38 +00:00
4135726772 Abo-Uebersichten: Team-Ansicht konsolidiert, Monatsfilter, Wachstum & Dashboard-KPIs
- /user/abos/ot: Filter nach Eingangsmonat (start_date) ueber neuen UserAbo-Scope
- Konsolidierte Team-Abo-Ansicht nach Ebenen/Linien (Bestand, neue Abos im Monat,
  Eigen-/Kundenabos, Punkte je Ebene + Summen) via TeamAboOverviewService
- team/customers leitet auf konsolidierte Ansicht um; Navigation bereinigt
- Mitglieder-Detail-Modal mit Abo-Zusammenfassung (Eigen-Abo, Kundenabos,
  Punkte, Beginn, naechste Ausfuehrung, Ausfuehrungen)
- Wachstums-Tabelle (Jahr x Monate) + Liniendiagramm (neue Eigen-/Kundenabos)
- Dashboard-KPI-Kachel (Gesamtabos, Kundenabos, 1000-Punkte-Shops)
- i18n de/en/es/fr, Tests (Filter, Overview, Wachstum, Modal, Redirect)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 14:07:29 +00:00
65b867f8ee Abo Einmalprodukte: Phasen 5-7, konfigurierbare Sperrfristen, Lokalisierung & Dashboard
Rechnung/Lieferschein:
- PDF-Gruppierung Abo- vs. Einmal-Artikel (is_abo_addon), rueckwaertskompatibel
- Vorschau-Command sales:preview-invoice (umgeht Test-System-Gate, ohne Rechnungsnummer)

Reminder-Mail (vorbereitet, Cron inaktiv):
- MailAboExecutionReminder + Template, abo:send-execution-reminder, Spalte onetime_reminder_sent_for
- Berater -> Sales Center, Kunde -> Portal; Vorschau abo:preview-reminder
- Datum-Fix: formatDateDB() statt formatDate() (ICU-Format)

Sperrfristen konfigurierbar (Admin-Settings):
- abo-lock-days-change (Liefertag + Abo-Produkte), abo-lock-days-pause-cancel
- Meldungen mit :lock-Platzhalter; Pausieren/Kuendigen vom Change-Lock entkoppelt
- Abo-Produkt-Aenderungen im Lock-Fenster server- + clientseitig eingefroren
- Selbstbedienung: Add-only/Lock nach $view (kein Admin-Bypass), Stale-Cache-Fix beim Mengen-Update

Dashboard:
- Einmalprodukte-Card + Team-Abo-KPI-Card, News-Card responsiv (col-xl-6)

Lokalisierung:
- Sprachdateien en/es/fr vollstaendig mit de synchronisiert (inkl. marketingplan/portal)
- LocalizationSyncTest sichert Key-Gleichheit ab

Tests: AboOneTimeInvoiceGrouping, AboExecutionReminder, AboLockDaysSetting,
AboOnetimeDashboardAndMail, PreviewInvoiceCommand, TeamAboOverview + erweiterte View-Tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:38:11 +00:00
9f996a7f1b Abo: Extern erledigt zaehlt als ausgefuehrte Abo-Lieferung
getCountPaidOrders() zaehlt jetzt bezahlte ODER extern erledigte (Status 8)
Bestellungen als ausgefuehrte Lieferung. "Extern erledigt" = Lieferung
ausgefuehrt und Punkte vergeben, daher muss der Lieferungs-Zaehler hochgehen
(Anzeige "Abo-Lieferungen X / Mindestlaufzeit" sowie canCancelAbo), obwohl
paid=false bleibt (Inkasso laeuft ausserhalb des Systems).

Der Incentive-Zaehler (qualified_abos via IncentiveTracker::trackAboActivated)
lief bereits korrekt; Test ergaenzt diese Absicherung explizit.

Tests: getCountPaidOrders 0 -> 1 nach Erledigen, Incentive qualified_abos = 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:57:29 +00:00
6720d99e08 Admin-Abos: Filterleiste und Textsuche nach PayOne-ID
Neue serverseitige, pro Admin persistente Filter auf /admin/abos:
- Status (echte Abo-Status aus UserAbo::$statusTypes, nur vorhandene Werte)
- Aktiv / Inaktiv
- Ist fuer (Berater/Kunde, is_for me/ot)
- Zahlung (Kreditkarte/PayPal ueber clearingtype cc/wlt)
- Startdatum (Monat + Jahr, optisch gruppiert; whereMonth/whereYear)
- Naechste Ausfuehrung (feste Liefertage 5/10/20/25 ueber abo_interval)
- Berater-Zuordnung (Member-Dropdown, Proportionen wie /admin/business/points)
- Reset-Button (korrigiert: bleibt auf admin_abos, Label ausserhalb des Buttons)

Layout in zwei Zeilen, damit nichts zusammengequetscht wird.

Textsuche der Tabelle: payone_userid via filterColumn suchbar gemacht
(addColumn-Spalten schliesst Yajra sonst von der globalen Suche aus).

Lang-Keys tables.month/year/all/inactive (de/en) ergaenzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 09:54:50 +00:00