- 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>
Beim Umstieg auf Git-Deploy mit dem Live-Server abgeglichen:
- resources/lang/de/website.php (Text Online-Streitbeilegung) und
resources/lang/es.json (DIV): im Admin auf Live geaendert
- web/templates partner, ueber-uns, erreichbarkeit: lagen nur auf Live,
ueber SiteController erreichbar, /partner im Footer verlinkt
- Migrationen create_dhl_shipments, create_user_invoice_credits,
create_users_sales_volumes: auf Live ausgefuehrt, fehlten im Repository
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
- 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>