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>
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>
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>
- 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>