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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- Ö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>
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>
- 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>
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>
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>
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>
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>
Neue Admin-Funktion fuer angehaltene Abo-Bestellungen, die der Kunde
ausserhalb des Systems geliefert und verrechnet hat.
Aktion (AboExternalCompletionService):
- Punkte gutschreiben wie bei eingegangener Zahlung (SalesPointsVolume,
idempotent gegen Doppelbuchung)
- UserAboOrder auf neuen gruenen Status 8 "extern erledigt"; paid bleibt false
- Bestellung: shipped=2 (Lieferung bestaetigt), keine Rechnung/kein Lieferschein
- Blockade aufheben: Abo zurueck auf status=2 (okay), aktiv, next_date neu,
damit der naechste Termin regulaer laeuft
- wie bei regulaerer Ausfuehrung: IncentiveTracker + AboOneTimeService-Purge
Einstiegspunkte: Button + Bestaetigungs-Modal auf der Kunden-Bestelldetailseite
(SalesController) und auf der Abo-Detailseite (Admin\AboController@externDone,
Route admin_abos_extern_done). Status 8 (gruen) in UserAbo/UserAboOrder, Labels
de/en.
Tests: tests/Feature/AboExternalCompletionTest (Happy Path, Idempotenz,
bezahlt-/Nicht-Abo-Guards, Abo-Detail-Einstieg) - 5 gruen.
Zusaetzlich: docs/2026-06-11-test-suite-vorbestehende-fehler.md - Diagnose der
93 vorbestehenden (von master geerbten) Test-Suite-Fehler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- UserMakeOrder: bestaetigte Einmal-Artikel in den Yard, is_abo_addon
auf ShoppingOrderItem; amount bleibt reiner Abo-Betrag (Reihenfolge)
- AboOneTimeService::purgeAfterExecution: loescht alle Einmal-Artikel
und rechnet Comp-Produkte neu - nur im Erfolgszweig (Cron + Retry)
- User-Retry in Sales Center und Portal mit Berechtigungspruefung,
gemeinsames Confirm-Modal; Admin-Retry unveraendert
- Tests: AboMakeOrderOneTimeTest, AboUserRetryTest; Plan-Doku Phase 4
Co-authored-by: Cursor <cursoragent@cursor.com>