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>
This commit is contained in:
Kevin 2026-07-09 14:45:17 +00:00
parent e1da90ff91
commit dd1c8f9a1e
68 changed files with 2856 additions and 236 deletions

View file

@ -1,6 +1,21 @@
# Entwicklungsplan: Stripe als Zahlungsanbieter (PAYONE bleibt als Option)
> **Status:** In Arbeit (S0 ✓, S1 ✓, S2 ✓, S3 ✓ inkl. Browser-E2E — **Notfall-Go-Live wegen PAYONE-Ausfall, Abos eingefroren, s. §7.1**; als Nächstes S4) · **Erstellt:** 2026-07-08 · **Zuletzt aktualisiert:** 2026-07-08
> **Status:** In Arbeit (S0 ✓, S1 ✓, S2 ✓, S3 ✓ — **live in Produktion**; S4 ✓ **E2E komplett** (Abschluss, Cron, Decline→Recovery, authentication_required); S5 ✓ „Zahlungsart ändern" inkl. E2E über CRM + Kunden-Link (nur Portal-Pfad ohne Sichttest, gleicher Code-Pfad); S7.6 ✓ Zahlarten-Kommunikation — **vor Abo-Freigabe:** Deployment + 3 Migrationen, dann `abo-payments-frozen` aufheben, §16) · **Erstellt:** 2026-07-08 · **Zuletzt aktualisiert:** 2026-07-09
>
> **Lage-Update 2026-07-09 (Stakeholder):** Das PAYONE-Konto ist **wieder freigeschaltet**.
> Daraus ergeben sich folgende verbindliche Entscheidungen (Details §7.2):
> 1. **Auslaufmodell statt Migration:** Bestehende PAYONE-Abos laufen unverändert über
> PAYONE weiter (Kunden müssen nicht aktiv werden). **Neue** Abos werden über Stripe
> angelegt; wird das **Zahlungsmittel eines Abos geändert** (Recovery-Zahlung → S4,
> Kreditkarte-ändern-Flow → S5), wechselt das Abo dabei auf Stripe. PAYONE läuft so
> Stück für Stück im Hintergrund aus.
> 2. **Vorkasse (PAYONE) wieder anbieten:** Der Checkout zeigt neben der Stripe-Online-
> Zahlung wieder „Vorkasse (PAYONE)" (Setting `payment-vorkasse-hidden` → aus).
> 3. **Zahlarten-Kommunikation** (→ S7.6): Stripe-Zahlarten klar benennen, möglichst
> mit Logos — im Checkout, auf den Shop-Seiten, im Footer (Zahlungsmöglichkeiten)
> und auf der Seite `/zahlungsarten`. ✅ **umgesetzt 2026-07-09** (§11, Umsetzungsstatus S7.6).
> 4. **Rückbuchungen & Stornierungen erfassen** — als eigener, **nachgelagerter** Punkt
> aufgenommen (→ §12.1 / Phase S9).
> **Test-Server:** Schalter `payment-active-provider` steht auf **stripe** (seit 2026-07-08, für den Manualtest); zurückstellen: `Setting::setContentBySlug('payment-active-provider', 'payone', 'text')`.
> **Begleitdokumente:** `ANALYSE-IST-ZUSTAND.md` (Codebase-Analyse) · `STRIPE-KONZEPT.md` (Architektur & API-Mapping)
> **Vorarbeiten:** `dev/2026-06-09-payments/ENTWICKLUNGSPLAN-ZAHLUNGSANBIETER.md` (Phase R ✅), `dev/2026-06-25-kreditkarte-update/`
@ -284,7 +299,7 @@ unbehandelte Typen mit `200 ignored`).
---
## 7. Phase S3 – Einmalzahlung im Checkout ⭐ · Status: **UMGESETZT** (2026-07-08, Browser-E2E offen)
## 7. Phase S3 – Einmalzahlung im Checkout ⭐ · Status: **ERLEDIGT & LIVE** (2026-07-09)
### Ziel
Steht der Schalter auf `stripe`, laufen **neue Einmalzahlungen** (Shop, Zahlungslinks)
@ -372,9 +387,9 @@ Payment-Regression (PAYONE + Abo-Retry/Recovery, 66 Tests) grün; Blade kompilie
unabhängig von S3.)*
### Akzeptanzkriterien
- [ ] **Browser-E2E (Manualtest, offen):** Checkout → Redirect zu Stripe → Zahlung (inkl. 3DS-Testkarte) → Rücksprung → Webhook → Order bezahlt → Rechnung aus dem System.
- [ ] Apple Pay/Google Pay erscheinen auf fähigen Geräten ohne Zusatzcode; Zahlarten lassen sich im Stripe-Dashboard zu-/abschalten, ohne Deploy.
- [ ] Abbruch bei Stripe führt sauber in den Checkout zurück (Manualtest).
- [x] **Browser-E2E:** Checkout → Redirect zu Stripe → Zahlung → Rücksprung → Webhook → Order bezahlt → Rechnung aus dem System. *(2026-07-08 im Testmodus; seit Go-Live in Produktion bestätigt — Stakeholder 2026-07-09: „steht und funktioniert".)*
- [x] Apple Pay/Google Pay erscheinen auf fähigen Geräten ohne Zusatzcode; Zahlarten lassen sich im Stripe-Dashboard zu-/abschalten, ohne Deploy.
- [x] Abbruch bei Stripe führt sauber in den Checkout zurück.
- [x] Umschalten des Schalters wechselt ausschließlich neue Checkouts (per Test nachgewiesen).
---
@ -425,40 +440,61 @@ Alle Schalter sind damit ohne Tinker im Admin schaltbar (bestehender
CC-Update-Freeze (`updateCreditCard` → Error mit Hinweistext), Manager-Defaults aus —
in `StripeCheckoutTest.php`; Karten-Update-Regressionssuite (17 Tests) unverändert grün.
### Go-Live-Checkliste Produktion
### Go-Live-Checkliste Produktion — ✅ durchgeführt am 2026-07-08 abends
1. [ ] Deployment des aktuellen Stands (Branch `payment/phase-r-payone-refactoring`).
2. [ ] `php artisan migrate` (3 neue Migrationen: Provider-Spalten, `stripe_webhook_events`).
3. [ ] Prod-`.env`: `STRIPE_KEY`/`STRIPE_SECRET` = **Live-Keys** (`pk_live_…`/`sk_live_…`), `STRIPE_MODE=live`.
4. [ ] Stripe-Dashboard (**Live-Modus**): Zahlarten aktivieren; Webhook-Endpoint
1. [x] Deployment des aktuellen Stands (Branch `payment/phase-r-payone-refactoring`).
2. [x] `php artisan migrate` (damals 3 neue Migrationen: Provider-Spalten, `stripe_webhook_events`).
3. [x] Prod-`.env`: `STRIPE_KEY`/`STRIPE_SECRET` = **Live-Keys** (`pk_live_…`/`sk_live_…`), `STRIPE_MODE=live`.
4. [x] Stripe-Dashboard (**Live-Modus**): Zahlarten aktivieren; Webhook-Endpoint
`https://api.mivita.care/stripe/webhook` mit der Event-Liste aus §6 anlegen;
dessen Signing-Secret als `STRIPE_WEBHOOK_SECRET` in die Prod-`.env`.
5. [ ] Caches: `php artisan config:clear && php artisan view:clear` (bzw. Deploy-üblich `config:cache`).
6. [ ] Schalter setzen — im Admin unter **Einstellungen → „Zahlungen (Stripe / PAYONE)"**:
*Stolpersteine (beide gelöst): falsches Webhook-Secret → alle Events 400;
`charge.succeeded` war anfangs nicht abonniert → Zahlart-Anzeige blieb auf Fallback.*
5. [x] Caches: `php artisan config:clear && php artisan view:clear` (bzw. Deploy-üblich `config:cache`).
6. [x] Schalter setzen — im Admin unter **Einstellungen → „Zahlungen (Stripe / PAYONE)"**:
☑ Checkout-Wartungsmodus (nur Admins bestellen), ☑ Abo-Zahlungen einfrieren,
☑ Vorkasse ausblenden (**wichtig:** Vorkasse autorisiert über PAYONE und würde
beim Ausfall crashen), Anbieter = **Stripe**.
7. [ ] Smoke-Test **als Admin bei aktivem Wartungsmodus**: echte Bestellung (Live-Karte)
→ Stripe-Redirect → bezahlt → Rechnung; ggf. im Stripe-Dashboard erstatten.
Gegenprobe im Inkognito-Fenster: Besucher sehen die Wartungsmeldung.
8. [ ] Checkout öffnen: **Wartungsmodus abhaken** — Einmalzahlungen laufen für alle über Stripe.
9. [ ] Kontrolle: `storage/logs/stripe.log`, `stripe_webhook_events`, Payment-Dashboard.
10. [ ] Nach S4: `abo-payments-frozen` zurücksetzen.
7. [x] Smoke-Test **als Admin bei aktivem Wartungsmodus**: echte Bestellungen (u. a.
57831, 57832 — müssen noch storniert/bereinigt werden, Statistik/Provision!).
8. [x] Checkout geöffnet: Einmalzahlungen laufen seit 2026-07-08 abends für alle über Stripe.
9. [x] Kontrolle: `storage/logs/stripe.log`, `stripe_webhook_events`, Payment-Dashboard.
10. [ ] **Nach S4/S5 (JETZT offen, → §16):** Deployment + 3 neue Migrationen, dann
`abo-payments-frozen` aufheben und Vorkasse wieder einblenden.
---
## 8. Phase S4 – Abos über Stripe
## 7.2 PAYONE wieder freigeschaltet (2026-07-09): Auslaufmodell & Vorkasse
**Lage:** Das PAYONE-Konto ist wieder aktiv. Entscheidungen Stakeholder (2026-07-09):
| # | Entscheidung | Umsetzung |
|---|--------------|-----------|
| 1 | **Bestands-Abos bleiben auf PAYONE** — Kunden müssen nicht aktiv werden, die alte Verknüpfung läuft weiter. Keine aktive Migrationskampagne (S8 damit vorerst obsolet). | Bereits durch S1 abgedeckt (`providerFor($abo)` liest die Spalte). Nach S4-Abschluss: `abo-payments-frozen` → aus, der Cron bucht PAYONE-Abos wieder regulär ab. |
| 2 | **Neue Abos → Stripe.** Abo-Erstzahlung im Checkout läuft über Stripe (Schalter steht auf `stripe`). | Phase S4 (dieser Sprint). |
| 3 | **Zahlungsmittel-Änderung → Stripe.** Wo ein Abo-Zahlungsmittel neu hinterlegt wird — Recovery-Zahlung (S4) und Kreditkarte-ändern-Flow (S5) — wechselt das Abo dabei von PAYONE auf Stripe. So läuft PAYONE Stück für Stück im Hintergrund aus. | Recovery: S4. CC-Update: S5 (der Flow wird für PAYONE-Abos zum Stripe-SetupIntent-Flow → sanfte Migration; zieht den S8-Mechanismus faktisch vor). |
| 4 | **Vorkasse (PAYONE) wieder anbieten.** Der Checkout zeigt neben „Online-Zahlung" (Stripe) wieder Vorkasse — klar als „Vorkasse (PAYONE)" benannt. | Ops: Setting `payment-vorkasse-hidden` im Admin abhaken. Code: Checkout-Label → `payment.prepayment_payone` („Vorkasse (PAYONE)"). |
**Freeze-Aufhebung (nach S4-Deployment):** `abo-payments-frozen` → aus. Danach:
Bestands-PAYONE-Abos buchen wieder über PAYONE ab (Konto ist wieder frei), neue
Abos entstehen mit `payment_provider='stripe'`, eingefrorene Abbuchungen werden über
das unveränderte `next_date` automatisch nachgeholt.
---
## 8. Phase S4 – Abos über Stripe · Status: **ERLEDIGT** (2026-07-09, Testmodus-E2E komplett; offen nur Prod-Rollout/Freeze-Aufhebung → §16)
### Ziel
Neue Abos: Erstzahlung im Checkout mit Speicherung des Zahlungsmittels; wiederkehrende
Abbuchungen über den bestehenden Cron, off-session. Bestehende PAYONE-Abos laufen
unverändert über PAYONE.
unverändert über PAYONE (Auslaufmodell, §7.2). Recovery-Zahlungen laufen über Stripe
und ziehen das Abo dabei auf Stripe um.
### Aufgaben
| # | Aufgabe |
|---|---------|
| S4.1 | Abo-Erstzahlung: Checkout Session (`mode=payment`) mit `setup_future_usage=off_session` + `customer`; Zahlarten auf **wiederverwendbare** Methoden eingeschränkt (Konzept §3.2; Klarna/Amazon Pay nur nach positiver N1-Prüfung); nach `succeeded` PaymentMethod-ID aus dem PaymentIntent ermitteln. |
| S4.1 | Abo-Erstzahlung: Checkout Session (`mode=payment`) mit `setup_future_usage=off_session` + `customer_creation=always`; nach `succeeded` PaymentMethod-/Customer-ID aus dem PaymentIntent ermitteln. **N1 damit gelöst:** Stripe filtert bei gesetztem `setup_future_usage` die Hosted-Checkout-Zahlarten automatisch auf wiederverwendbare Methoden (Dynamic Payment Methods) — keine eigene Whitelist nötig, Klarna/Amazon Pay erscheinen nur, wenn Stripe sie off-session-fähig anbietet. Gilt auch für Recovery-Checkouts (`recovery_user_abo_id`): Zahlung on-session, Zahlungsmittel wird gespeichert, Abo wechselt auf Stripe (§7.2 Nr. 3). |
| S4.2 | `AboHelper::createNewAbo()`: bei Stripe-Erstzahlung `payment_provider='stripe'`, `stripe_customer_id`, `stripe_payment_method_id` (statt `payone_userid`/`carddata`) übernehmen; maskierte Anzeige-Daten (Brand, letzte 4, Ablauf) als Anzeige-Metadaten speichern. |
| S4.3 | `StripePaymentService::chargeAbo()`: off-session PaymentIntent (`customer`, `payment_method`, `off_session=true`, `confirm=true`), Idempotency-Key = `user_abo_order`-Referenz (Schutz gegen Doppel-Abbuchung bei Cron-Retry). |
| S4.4 | `UserMakeOrder::makePayment()`: Provider-Dispatch über `PaymentProviderManager::providerFor($abo)` — PAYONE-Zweig byte-identisch. |
@ -466,36 +502,184 @@ unverändert über PAYONE.
| S4.6 | Abo-Ansichten (CRM `/user/abos`, Portal, Admin `AboController`): Provider + maskiertes Zahlungsmittel anzeigen; PAYONE-Felder bleiben für PAYONE-Abos maßgeblich. |
| S4.7 | Abo-Retry (`AboRetryPaymentService`) provider-fähig (nutzt `chargeAbo` des Abo-Providers). |
### Tests
- Erstzahlung → `UserAbo` mit Stripe-Feldern; PAYONE-Felder leer.
- Cron-Abbuchung Stripe-Abo: Erfolg (Order paid, `user_abo_orders` fortgeschrieben), Decline (Status 3), Idempotenz (doppelter Cron-Lauf bucht nicht doppelt ab).
- Gemischte Bestände: ein PAYONE-Abo + ein Stripe-Abo im selben Cron-Lauf → jeweils richtiger Gateway-Aufruf.
### Vorab-Designentscheidungen (2026-07-09)
- **Transport der Stripe-IDs:** Neue Spalten `stripe_customer_id`, `stripe_payment_method_id`
und `user_abo_id` auf `shopping_payments`. Der Webhook (`markPaid`) schreibt Customer/
PaymentMethod an die Zahlung, bevor der Paid-Pfad läuft; `createNewAbo()`/
`advanceAboAfterRecoveryPayment()` übernehmen sie von dort ans Abo.
- **Race Browser-Return vs. Webhook:** Der Checkout-Erfolgs-Return ruft `createNewAbo()`
ggf. auf, bevor der Webhook die Stripe-IDs geliefert hat. Für Stripe-Zahlungen legt
`createNewAbo()` das Abo daher **nur** an, wenn die IDs bereits an der Zahlung stehen —
sonst übernimmt der Webhook-Paid-Pfad die Anlage (einzige Wahrheit, wie bei S3).
- **Race Cron-Transaktion vs. Webhook:** Der Abo-Cron bucht **innerhalb einer
DB-Transaktion** ab — der Webhook kann eintreffen, bevor die `shopping_payments`-Zeile
committet ist. Zwei Sicherungen: (1) `shopping_payments.user_abo_id` markiert
Folgeabbuchungen, `createNewAbo()` überspringt sie (kein Duplikat-Abo, falls der Webhook
vor dem `user_abo_orders`-Insert des Crons verarbeitet wird); (2) trägt ein Event unsere
`metadata.shopping_payment_id`, ist die Zahlung aber (noch) nicht auffindbar, antwortet
der Webhook **non-2xx** → Stripe stellt erneut zu, statt das Event als verarbeitet zu
begraben. Fremd-Events ohne unsere Metadata werden weiterhin gespeichert + 200.
- **Recovery auch im Webhook-Pfad:** `Payment::paymentStatusPaidAction()` ruft für
Recovery-Bestellungen jetzt `advanceAboAfterRecoveryPayment()` statt `createNewAbo()`
auf (vorher nur im Browser-Return — latenter Duplikat-Abo-Bug, wenn der PAYONE-TSOK
vor dem Redirect lief; provider-neutral gefixt).
- **Idempotency-Key** der Cron-Abbuchung: `abo-{userAboId}-order-{shoppingOrderId}` —
schützt gegen Doppel-Abbuchung bei Netzwerk-Retry innerhalb desselben Laufs.
### Umsetzungsstatus (2026-07-09)
| # | Status |
|---|--------|
| S4.1 | ✅ `initiateCheckoutPayment()`: Abo-/Recovery-Bestellungen → `customer_creation=always` + `payment_intent_data.setup_future_usage=off_session`; Einmalzahlungen unverändert. Zahlarten-Filterung an Stripe delegiert (Dynamic Payment Methods, N1 gelöst). `CheckoutController::stripeCheckoutActive()` ohne Abo-/Recovery-Ausschluss; Abo-Hinweistext (`payment.stripe_abo_hint`) unter der Online-Zahlung. |
| S4.2 | ✅ `createNewAbo()`: Stripe-Zweig setzt `payment_provider='stripe'`, `stripe_customer_id`, `stripe_payment_method_id`, `payone_userid=0`; ohne IDs (Browser-Return vor Webhook) wird die Anlage dem Webhook überlassen. Neue Migration `2026_07_09_090000_add_stripe_abo_columns_to_shopping_payments` + `captureOffSessionPaymentMethod()` im Webhook (PI-Event direkt, Event-Store-Backfill, API-Retrieve als letzter Ausweg). |
| S4.3 | ✅ `StripePaymentService::chargeAbo()`: off-session PaymentIntent (`customer`, `payment_method`, `off_session`, `confirm`), Idempotency-Key `abo-{aboId}-order-{orderId}`, Transaktion als `authorization` protokolliert, Ergebnis als `AboChargeResult`. |
| S4.4 | ✅ `UserMakeOrder::makePayment()`: Dispatch via `providerFor($abo)` — Stripe → `StripeGateway` (neu, Adapter wie PayoneGateway), PAYONE-Zweig byte-identisch; Response-Format (`APPROVED`/`ERROR`/`PENDING`) unverändert für Cron/Retry. |
| S4.5 | ✅ Fehler-Mapping: `CardException`/`ApiErrorException` → ERROR (Cron setzt Status 3 + Pay-now-Mail wie bei PAYONE); Kunden-Meldungen für `card_declined`, `insufficient_funds`, `expired_card`, `incorrect_cvc`, `authentication_required` (bestehende `payment.payment_error_reasons.*`-Keys, neu `payment.stripe_abo_*`); PI-Status `processing` → PENDING (next_date bleibt stehen, Webhook schließt ab). |
| S4.6 | ✅ `UserAbo::getPaymentType()` Stripe-aware (nutzt `ShoppingPayment::stripeLabelFromSummary()`, neu public static); `UserAbo::formatMaskedCard()` versteht die Stripe-Summary („Visa •••• 4242 (04/2031)"); Portal-Abo-Kopf zeigt bei Stripe die Abo-Nummer statt `#0`. *Offen (kosmetisch → S7.3): Provider-Badge in Admin-Listen, `payone_userid`-Spalte im Admin-Datatable.* |
| S4.7 | ✅ implizit: `AboRetryPaymentService` delegiert an `UserMakeOrder::makePayment()` → Dispatch greift. |
**Zusätzliche Absicherungen (Umsetzung der Vorab-Designentscheidungen):**
- `Payment::paymentStatusPaidAction()` behandelt Recovery-Bestellungen jetzt provider-neutral
(`advanceAboAfterRecoveryPayment` statt `createNewAbo`) — fixt den latenten
Duplikat-Abo-Bug auch für PAYONE.
- `createNewAbo()` überspringt Recovery- und Cron-Folgezahlungen (`recovery_user_abo_id` /
`shopping_payments.user_abo_id`) grundsätzlich.
- Webhook: Event mit unserer `metadata.shopping_payment_id`, dessen Zahlung (noch) nicht
sichtbar ist → `503 payment_not_yet_visible` (Stripe-Retry); Fremd-Events weiter `200`.
- `advanceAboAfterRecoveryPayment()`: Stripe-Recovery zieht das Abo auf Stripe um
(Provider + IDs), PAYONE-Felder bleiben als Historie stehen; ohne IDs wird die
Fortschreibung dem Webhook überlassen (Race Browser-Return).
### Tests (2026-07-09)
`tests/Feature/StripeAboTest.php` *(neu)*: **15 Tests, 70 Assertions** — Session-Parameter
(off-session nur für Abo/Recovery), Abo-Anlage aus dem Webhook (Stripe-Felder, `payone_userid=0`),
Browser-Return-Race (Anlage wartet auf Webhook), Cron-Race (kein Duplikat-Abo; 503 → Retry →
processed), Recovery-Umzug PAYONE→Stripe, `chargeAbo` (Erfolg inkl. Idempotency-Key,
Decline-Mapping, fehlendes Zahlungsmittel ohne API-Call), Cron-Dispatch (APPROVED/ERROR),
Anzeige-Labels. `PaymentProviderManagerTest` angepasst (Stripe-Gateway existiert jetzt;
Cron-Guard-Test → Dispatch-Test).
Regression: Stripe-Suite gesamt **59 passed (196 Assertions)**; PAYONE- + CC-Update-Suite
**50 passed (172 Assertions)**; Abo-Retry/Recovery **20 passed** — der eine Fehler in
`AboPendingInitialPaymentTest` („Session store not set", View-Render mit `Form::open`) ist
**vorbestehend auf HEAD** (per `git stash` verifiziert), unabhängig von S4. Blade-Kompilat
(`view:cache`) grün, `./vendor/bin/pint --dirty` grün.
*Abgleich mit dem ursprünglichen Testplan:* Erstzahlung ✅ · Cron-Abbuchung
Erfolg/Decline/Idempotenz ✅ · Recovery ✅ · Gemischte Bestände ▶ implizit über
Dispatch-Tests + PAYONE-Regression, expliziter Mischlauf gehört in den Testmodus-E2E.
### Akzeptanzkriterien
- [ ] Testmodus Ende-zu-Ende: Abo-Checkout → n Tage später Cron-Abbuchung off-session erfolgreich, Rechnung systemseitig.
- [x] Testmodus Ende-zu-Ende ✅ **komplett 2026-07-09** (alles an Abo #442 verprobt):
1. Browser-Abo-Abschluss → Abo mit Provider `stripe`, Customer-/PM-ID, Status 2, Order paid via Webhook.
2. Cron-Abbuchung `--force` → off-session PI erfolgreich, Order bezahlt, `next_date` fortgeschrieben, kein Duplikat-Abo.
3. Abbuchung über per S5-Flow gewechselte Zahlart → erfolgreich (3DS-Karte `…3155`: Setup mit SCA, Folgeabbuchungen als MIT genehmigt — erwartetes Stripe-Verhalten).
4. **Decline** (Karte `…0341`) → Order failed, Abo Status 3 (+ Pay-now-Mail).
5. **Recovery-Checkout** → Recovery-Order bezahlt (`recovery_user_abo_id` gesetzt), Abo zurück auf Status 2.
6. **authentication_required** (Karte `…3184`, verlangt 3DS bei jeder Transaktion) → off-session Decline, Abo Status 3 — der S6.1-Fall mündet korrekt im Recovery-Werkzeug.
*(Rechnung systemseitig: auf .test bewusst deaktiviert, Invoice-Gate; Mischlauf PAYONE+Stripe verifiziert sich nach Freeze-Aufhebung in Produktion.)*
- [ ] PAYONE-Abos zu 100 % unberührt (Regressionssuite + Stichprobe Staging).
- [ ] Nach Freeze-Aufhebung: PAYONE-Bestandsabos buchen wieder ab, neue Abos entstehen mit `payment_provider='stripe'`.
### Befund aus dem Cron-E2E (2026-07-09): Race trat real auf → `stripe:process-pending-events` (S6.4 vorgezogen)
Beim ersten echten Cron-Lauf (`user:make_abo_order --abo=442 --force`) trat das in den
Designentscheidungen antizipierte **Cron-Race** sofort ein: Der Webhook
(`payment_intent.succeeded`) kam ~5 s nach Zahlungsanlage — die Cron-DB-Transaktion war
noch offen → korrekt **503 `payment_not_yet_visible`**. Aber: Die **Stripe CLI
(`stripe listen`) stellt non-2xx NICHT erneut zu** (anders als der Dashboard-Endpoint in
Produktion) → das Event blieb unverarbeitet, die Bestellung „unbezahlt", obwohl die
Abbuchung bei Stripe erfolgreich war.
**Fix (Root Cause, deckt auch Prod-Retry-Latenz ab):** Neues Wartungs-Command
**`stripe:process-pending-events`** (`--event=` für Einzelfälle) — verarbeitet
gespeicherte Events ohne `processed_at` über `StripeWebhookHandler::processStoredEvent()`
nach (Signatur wurde beim Empfang geprüft; weiterhin nicht zuordenbare Events bleiben
liegen). Verdrahtung: (1) läuft automatisch **am Ende von `user:make_abo_order`**
(alle Zahlungen committet), (2) **Scheduler alle 10 Minuten** (`Kernel.php`,
withoutOverlapping). Damit ist S6.4 im Kern vorgezogen; der geplante
`stripe:sync-payment-status` (aktiver API-Abgleich bei komplett verlorenem Webhook)
bleibt für S6 offen. Nebeneffekt beim ersten Lauf: 4 liegengebliebene
`charge.succeeded`-Events vom Go-Live-Tag nachgeholt (Zahlart-Anzeige).
### Befund aus dem Browser-E2E (2026-07-09): `clearingtype` war varchar(3)
Beim Verproben von Abo #442 fiel auf: `clearingtype` stand auf **`'str'`** — die Spalten
(`shopping_payments`, `user_abos`) waren `varchar(3)` (PAYONE-Kürzel), MySQL hat
`'stripe'` **still abgeschnitten** (betraf auch alle Live-Einmalzahlungen seit dem
S3-Go-Live; SQLite in der Testsuite erzwingt keine Länge, daher unentdeckt). Kein
Logik-Problem — alle Lese-Pfade prüfen `payment_provider`, nie `clearingtype==='stripe'`
(per Grep verifiziert). Fix: Migration `2026_07_09_120000_widen_clearingtype_for_stripe`
(beide Spalten → varchar(20) + Reparatur der `'str'`-Bestandszeilen; `down()` verkürzt
bewusst nicht). Auf der Dev-DB ausgeführt, Abo #442 repariert. **Muss beim
Prod-Deployment mitlaufen** (repariert dort die Live-`'str'`-Zeilen automatisch).
---
## 9. Phase S5 – Zahlungsmittel-Update über Stripe
## 9. Phase S5 – Zahlungsart ändern über Stripe · Status: **ERLEDIGT** (2026-07-09, E2E über CRM + Kunden-Link; nur Portal-Pfad ohne Sichttest)
### Ziel
Der Flow aus dem CC-Update-Projekt (2026-06-25) funktioniert für Stripe-Abos:
Zahlungsmittel erneuern **ohne Abbuchung**, nur durch den Abo-Inhaber.
**Scope-Erweiterung (Stakeholder 2026-07-09):** Nicht mehr nur „Kreditkarte
aktualisieren" — der Abo-Inhaber kann seine **Zahlungsart frei wechseln**: Karte
abgelaufen, PayPal-Konto weg → „Zahlungsart ändern" führt auf die Stripe-Hosted-Seite
(Session `mode=setup`), dort **freie Wahl** unter allen wiederverwendbaren Zahlarten
(Karte, PayPal, SEPA, … — Stripe filtert automatisch); die neue Methode wird **ohne
Abbuchung** am Abo verankert und gilt für alle folgenden Abbuchungen. Nur durch den
Abo-Inhaber (Policies aus dem CC-Update-Projekt 2026-06-25 unverändert).
Der Flow gilt für **alle Abos, auch PAYONE-Bestandsabos** (§7.2 Nr. 3): Wer die
Zahlungsart ändert, wechselt dabei auf `payment_provider='stripe'` (PAYONE-Felder
bleiben als Historie). Das ist die sanfte Auslauf-Migration; eine aktive
Migrationskampagne (S8) ist damit nicht mehr geplant. Der bestehende
PAYONE-CC-Update-Pfad bleibt im Code erhalten und greift nur noch, solange der
Anbieter-Schalter auf `payone` steht.
### Aufgaben
| # | Aufgabe |
|---|---------|
| S5.1 | `StripePaymentService::setupPaymentMethodUpdate()`: Checkout Session `mode=setup` (hosted, Redirect – konsistent zum Checkout-Flow); Einstieg über die bestehenden Card-Update-Views/Links (Verzweigung nach Abo-Provider). |
| S5.2 | `setup_intent.succeeded`-Webhook bzw. Return-Bestätigung: neue `stripe_payment_method_id` am Abo speichern, alte PaymentMethod am Customer detachen; `user_abo_card_changes`-Protokoll weiternutzen. |
| S5.3 | Konsistenz-Regeln aus dem CC-Update-Projekt übernehmen: `card_update_pending_at` vor externem Call setzen; API-OK → DB-Update; Fehlerfall → CRITICAL-Log + Incident (Alt-Plan §5.3.1). |
| S5.4 | `AboPaymentMethodService` provider-fähig; `AboCardUpdateLink`-Flow (Berater→Kunde-Link) für Stripe-Abos freischalten. |
| S5.1 | `StripePaymentService::setupPaymentMethodChange()`: Checkout Session `mode=setup` (hosted, Redirect — konsistent zum Checkout-Flow) mit Customer (vorhandene `stripe_customer_id` des Abos oder neu via `StripeCustomerService`/Abo-Daten) und `setup_intent_data.metadata.user_abo_id`; freie Zahlartenwahl bei Stripe. |
| S5.2 | Webhook `checkout.session.completed` (`mode=setup`) / `setup_intent.succeeded`: Abo via `metadata.user_abo_id` auflösen → `payment_provider='stripe'`, `stripe_customer_id`, `stripe_payment_method_id`, `clearingtype='stripe'`, Anzeige-Summary (`carddata`) aus der PaymentMethod; alte Stripe-PaymentMethod am Customer detachen (best effort); `user_abo_card_changes`-Protokoll weiternutzen. |
| S5.3 | Konsistenz-Regeln aus dem CC-Update-Projekt übernehmen: `card_update_pending_at` vor Redirect setzen; Webhook-OK → DB-Update + Flag löschen; Fehlerfall → CRITICAL-Log (Alt-Plan §5.3.1). |
| S5.4 | Einstiege umstellen (Verzweigung: Schalter `stripe` → Stripe-Setup-Flow, sonst bestehender PAYONE-CC-Pfad): Portal „Zahlungsart ändern", CRM (Berater-eigenes Abo), `AboCardUpdateLink`-Flow (Berater→Kunde-Link) — Formular-Partial zeigt statt Kartenfeldern einen „Zahlungsart bei Stripe ändern"-Button; gilt für PAYONE- UND Stripe-Abos. |
### Tests
- SetupIntent-Erfolg aktualisiert nur das Ziel-Abo; keine Abbuchung entsteht.
- Nicht-Inhaber wird abgewiesen (Policy unverändert).
- Pending-Flag/Inkonsistenz-Pfad (API ok, DB-Fehler) erzeugt Incident.
### Umsetzungsstatus (2026-07-09)
| # | Status |
|---|--------|
| S5.1 | ✅ `StripePaymentService::createPaymentMethodChangeSession()` (mode=setup, Metadata `user_abo_id`/`source` auf Session + SetupIntent); `ensureAboStripeCustomer()` legt für PAYONE-Bestandsabos den Stripe-Customer an und persistiert ihn sofort (Provider bleibt PAYONE bis zur Verankerung). |
| S5.2 | ✅ Return-Pfad (`finalizeStripeChangeFromSession`, prüft Session-Zugehörigkeit via Metadata) UND Webhook `setup_intent.succeeded` rufen das idempotente `applyStripePaymentMethod()`: Provider→stripe, Customer-/PM-ID, `clearingtype='stripe'`, Anzeige-Summary (`retrievePaymentMethodSummary`), `user_abo_card_changes`-Eintrag; alte Stripe-PM wird best effort detached. |
| S5.3 | ✅ `card_update_pending_at` vor Session-Erstellung; Fehler → Spur zurück; DB-Write-Fehler nach Setup-Erfolg → CRITICAL-Log, Spur bleibt (Muster CC-Update-Projekt). Freeze-Schalter blockiert `beginStripeChange`. |
| S5.4 | ✅ Drei Einstiege verzweigen nach Schalter: Portal (`portal_abos_payment_method_stripe`), CRM me-Abo (`user_abos_payment_method_stripe`), Kunden-Link (`abo_card_update_stripe`); gemeinsames Partial `_payment_method_form` zeigt bei Stripe einen „Neue Zahlungsart wählen"-Button statt Kartenfeldern; Rückkehr über die bestehenden `…_return`-Routen (`?session_id={CHECKOUT_SESSION_ID}`). **Invariante sauber getrennt:** Model bleibt pur (`canUpdatePaymentMethod` = nur CC; neu `canChangePaymentMethodAnyType`), die Flow-Wahl liegt in `AboPaymentMethodService::canStartChange()` (Controller + Views umgestellt). `sendCardUpdateLink` erlaubt den Link im Stripe-Flow für jede Zahlart. Abo-Detail-Karten (CRM/Portal) zeigen die Zahlart jetzt via `getPaymentType()` auch für Nicht-CC-/Stripe-Abos. |
**Neue Lang-Keys:** `abo.payment_method_change_*`, `abo.current_payment_method` (de/en, du-Form).
Der PAYONE-CC-Pfad ist unverändert und greift bei Schalter `payone`.
### Tests (2026-07-09)
`tests/Feature/StripePaymentMethodChangeTest.php` *(neu)*: **10 Tests** —
*(Fixes nach Manualtests 2026-07-09: (1) Setup-Sessions mit dynamischen Zahlarten
verlangen `currency` — ergänzt, fest `eur`; (2) Erfolgs-/Fehlerseite des Kunden-Links
(`card_update_done`) und die Link-Mail (`AboCardUpdateLinkMail`) sprachen noch von
„Kreditkarte"/PAYONE — Texte jetzt flow-abhängig: „Zahlungsart aktualisiert" bzw.
Stripe-Wortlaut, neue Keys `abo.payment_method_change_done_*`/`…_link_mail_*`.)* —
Flow-Weiche (Nicht-CC-Abos nur im Stripe-Flow, terminale Abos nie), Setup-Session
(Params, Pending-Spur), Customer-Anlage für PAYONE-Abos (Provider bleibt payone),
Freeze-Guard, Verankern (PAYONE→Stripe-Umzug inkl. Anzeige/`card_changes`), Idempotenz +
Detach der alten PM, Webhook-Verankerung, Return-Pfad inkl. Abweisung fremder Sessions.
Regression: Stripe-Gesamtsuite + CC-Update- + Abo-Retry/Recovery-Suiten **99 passed
(303 Assertions)**, Blade-Kompilat grün, Pint grün.
*Ursprünglicher Testplan (Referenz):* Setup ohne Abbuchung ✅ · Nicht-Inhaber abgewiesen ✅
(Policies unverändert, per bestehender Suite) · Pending-/Inkonsistenz-Pfad ✅ (CRITICAL-Log;
Incident-Anbindung folgt mit S6.2).
### Akzeptanzkriterien
- [x] **Browser-E2E (Testmodus), CRM-Pfad:** ✅ 2026-07-09 — Abo #442, neue Karte über die Setup-Seite hinterlegt: neue PM-ID verankert, carddata „Visa •••• 3155", `card_update_pending_at` geräumt, **genau ein** `user_abo_card_changes`-Eintrag (source `crm` — Return-Pfad und Webhook liefen beide, Idempotenz griff), keine Abbuchung.
- [x] **Browser-E2E, Berater-Kunden-Link inkl. Nicht-Karten-Zahlart:** ✅ 2026-07-09 — Abo #440 über den Link auf **PayPal** umgestellt: Provider `stripe`, PM-ID verankert, carddata `{type: paypal}`, Link entwertet (`used_at`), Wechsel-Protokoll source `link`, Status 2. Dabei gefixt: Erfolgsseite + Link-Mail sprachen noch von „Kreditkarte" → Texte jetzt flow-abhängig („Zahlungsart aktualisiert"). *Offen: Portal-Pfad (gleicher Code-Pfad wie CRM, geringe Priorität).*
- [x] **PAYONE-Bestandsabo wechselt vollständig auf Stripe** (Felder + Provider), PAYONE-Felder bleiben als Historie: ✅ Abo #440 war ein PAYONE-CC-Abo (Wechsel-Historie vorhanden) und läuft jetzt über Stripe/PayPal.
---
@ -512,7 +696,7 @@ Payment-Dashboard kennt Stripe.
| S6.1 | `authentication_required` bei off-session: Abo auf Status 3 + Recovery-Checkout-Link (bestehender `AboRecoveryCheckoutService`); Recovery-Zahlung läuft on-session mit 3DS über den **Abo-Provider**; nach Erfolg `advanceAboAfterRecoveryPayment` wie gehabt. |
| S6.2 | `payment_intent.payment_failed` / `charge.dispute.created` / `charge.refunded` → `PaymentIncident` (Provider `stripe`) + Payment-Dashboard-Kacheln. |
| S6.3 | `payments:check-accounts` bzw. Uptime-Check um Stripe-Status ergänzen (API-Erreichbarkeit, offene `processing`-Zahlungen älter X Tage). |
| S6.4 | Wartungs-Command `stripe:sync-payment-status {paymentIntentId|--pending}` (Gegenstück zu `payone:*`-Reparatur-Commands): Status aktiv nachziehen, wenn ein Webhook verloren ging. |
| S6.4 | Wartungs-Command `stripe:sync-payment-status {paymentIntentId|--pending}` (Gegenstück zu `payone:*`-Reparatur-Commands): Status aktiv nachziehen, wenn ein Webhook verloren ging. **Teilweise vorgezogen (2026-07-09, s. §8):** `stripe:process-pending-events` verarbeitet gespeicherte, unverarbeitete Events nach (Cron-Ende + Scheduler alle 10 min). Offen bleibt der aktive API-Abgleich für komplett verlorene Webhooks. |
### Tests
- Failed-Webhook setzt Abo-/Order-Status korrekt, erzeugt Incident, keine Doppel-Mails.
@ -529,8 +713,55 @@ Payment-Dashboard kennt Stripe.
| S7.1 | **Schalter-UI** im Admin (Einstellungen): aktueller Provider, Umschalten mit Bestätigungsdialog + Hinweistext („wirkt nur auf neue Zahlungen"), `SysLog`-Eintrag. Nur berechtigte Admin-Rolle. |
| S7.2 | **Refunds:** `OrderWithdrawalController` (Widerruf) um automatische Erstattung erweitern — Provider der Ursprungszahlung entscheidet (Stripe: `Refund`-API; PAYONE: bestehender/manueller Weg). Teil-Erstattungen unterstützen. |
| S7.3 | Admin-Detailansichten (`ShoppingOrder`, `UserAbo`, Zahlungen): Provider-Badge, Stripe-IDs verlinkt zum Stripe-Dashboard (Deep-Link `https://dashboard.stripe.com/...`). |
| S7.4 | Payment-Dashboard: Umsatz/Fehlerraten je Provider; Übersicht „Abos je Provider" (Fortschritt einer späteren Migration ablesbar). |
| S7.4 | Payment-Dashboard (`/admin/payment-dashboard`) **für Stripe anpassen/optimieren** (Stakeholder 2026-07-09): Kacheln/Listen kennen bisher nur PAYONE — Stripe-Zahlungen korrekt ausweisen (Provider-Filter, Zahlart-Labels, Verlinkung ins Stripe-Dashboard), Umsatz/Fehlerraten je Provider, Übersicht „Abos je Provider" (Auslauf-Fortschritt ablesbar). |
| S7.5 | Doku: Betriebs-Runbook (Webhook neu ausliefern, Key-Rotation, Testmodus) in diesem Ordner ablegen. |
| S7.6 | **Zahlarten-Kommunikation** (Stakeholder 2026-07-09): Die über Stripe verfügbaren Zahlarten klar benennen, möglichst mit Logos — (a) im Checkout unter der „Online-Zahlung"-Option, (b) auf den Shop-Seiten, (c) im Footer bei den Zahlungsmöglichkeiten, (d) auf der Seite `/zahlungsarten`. Vorkasse als „Vorkasse (PAYONE)" ausweisen. **ERLEDIGT ✅ (2026-07-09), siehe Umsetzungsstatus unten.** |
### Umsetzungsstatus S7.6 (ERLEDIGT ✅, 2026-07-09)
Kommuniziertes Zahlarten-Set (per API gegen die aktive Stripe `PaymentMethodConfiguration`
und die Account-Capabilities verifiziert, Erweiterung auf Stakeholder-Wunsch 2026-07-09):
**Kreditkarte (Visa, Mastercard, American Express), PayPal, Apple Pay, Google Pay,
Klarna, Amazon Pay, Link (Stripe), EPS, Bancontact** sowie als Sammelgruppe „Weitere
Zahlarten": **BLIK, MB WAY, Pix, Satispay, Samsung Pay, Kakao Pay, Naver Pay, PAYCO** —
plus **Vorkasse (PAYONE)**. Sofortüberweisung (von Stripe eingestellt) und
SEPA-Lastschrift (nicht aktiviert) wurden von der `/zahlungsarten`-Seite und aus dem
Footer **entfernt**. ⚠️ Verifiziert gegen den **Test-Modus** — Test-/Live-Modus haben
getrennte Zahlarten-Konfigurationen; das Live-Dashboard sollte dasselbe Set anbieten.
- **Logo-Assets:** einheitliche Marken-SVGs (38×24-Kartenstil, Shopify-`payment_icons`-Set)
unter `public/images/payments-assets/`: `visa/mastercard/amex/paypal/applepay/googlepay/
klarna/amazonpay/eps/bancontact/blik/mbway/pix/satispay/samsungpay/kakaopay/naverpay/
payco.svg` sowie selbst erstellte Badges `vorkasse.svg` („€ →") und `link.svg`
(grünes Stripe-Link-Badge; kein Icon im Set verfügbar). Die alten PNGs bleiben
liegen (Checkout-Badges `stripe.png`/`vor.png`/`fnc.png` werden weiter genutzt).
- **Wiederverwendbares Partial:** `resources/views/web/layouts/includes/payment_logos.blade.php`
(Parameter `logoHeight`, `showVorkasse`, `showAll`, `wrapperClass`; `title`/`alt` je
Marke) — genutzt von Checkout, beiden Footern und `/zahlungsarten`. Standard
(Checkout/Footer): nur die Kern-Zahlarten + „···"-Icon (`more.svg`, Tooltip „Weitere
Zahlarten"); die regionalen Zahlarten (BLIK/MB WAY/Pix/…) erscheinen nur mit
`showAll = true` auf `/zahlungsarten` (Stakeholder-Feedback 2026-07-09).
- **(a) Checkout** (`web/templates/checkout.blade.php`): Logo-Leiste (ohne Vorkasse) unter
der Option „Online-Zahlung", darunter weiterhin die benannte Liste
(`payment.stripe_online_methods`, um Amazon Pay ergänzt). Vorkasse war bereits als
„Vorkasse (PAYONE)" ausgewiesen (S4).
- **(b) Shop-Seiten / (c) Footer:** beide Footer (`web/layouts/includes/footer.blade.php`,
`web/user/layouts/includes/footer.blade.php`) zeigen statt der 5 veralteten PNGs
(PayPal/Sofort/SEPA/CC/Vorkasse) die neue Logo-Leiste inkl. Vorkasse — damit auf allen
Shop-Seiten sichtbar.
- **(d) `/zahlungsarten`** (`web/templates/zahlungsarten.blade.php`): komplett neu —
Intro (Stripe als Zahlungsdienstleister) + Logo-Leiste, dann je Zahlart eine Zeile mit
Logo, Name und Kurzbeschreibung: Kreditkarte, PayPal, Apple Pay, Google Pay, Klarna,
Amazon Pay, Link (Stripe), EPS, Bancontact, Sammelzeile „Weitere Zahlarten"
(BLIK/MB WAY/Pix/Satispay/Samsung Pay/Kakao Pay/Naver Pay/PAYCO), Vorkasse (PAYONE).
Neue Lang-Keys `weborder.payment_method_*` in de (du-Form) und en; alte
Sie-Form-Texte ersetzt, `payment_method_direct*`/`_sepa*` entfernt (waren nur dort
verwendet).
- **Tests:** `tests/Feature/PaymentMethodsCommunicationTest.php` (27 Tests: Partial mit/ohne
Vorkasse, alle SVG-Assets vorhanden, alle de/en-Lang-Keys, „Vorkasse (PAYONE)"-Label).
- **Hinweis:** Die Seite ist statisch — sollte der Provider-Schalter im Notfall zurück auf
`payone` gestellt werden, stimmt die Kommunikation vorübergehend nicht (bewusst in Kauf
genommen; der Checkout selbst bleibt maßgeblich).
### Tests
- Schalter-UI: Berechtigung, Persistenz, SysLog.
@ -554,17 +785,37 @@ Payment-Dashboard kennt Stripe.
Provider trägt, entstehen keine Mischzustände. Offene Stripe-Zahlungen laufen über
Webhooks normal aus.
### Optionale Abo-Migration PAYONE → Stripe (nur nach Entscheid O8)
### Optionale Abo-Migration PAYONE → Stripe
> **Entscheid 2026-07-09 (§7.2): Auslaufmodell statt Kampagne.** PAYONE ist wieder
> aktiv, Bestandsabos laufen dort unbefristet weiter. Die „Migration" passiert passiv
> über die Zahlungsmittel-Änderung (S4 Recovery / S5 CC-Update) — eine aktive
> Einladungs-Kampagne (`abo:invite-stripe-migration`) ist **nicht mehr geplant** und
> bleibt hier nur als Option dokumentiert, falls PAYONE doch abgeschaltet werden soll.
- PAYONE-Tokens sind **nicht portabel** → Migration heißt: Kunde hinterlegt Zahlungsmittel
neu (SetupIntent, kein Charge) über den Card-Update-Link-Flow; danach
`payment_provider='stripe'` am Abo, PAYONE-Felder bleiben als Historie.
- Kampagnen-Command `abo:invite-stripe-migration` (Batch-Mails mit Link, Erinnerung,
Fortschritts-Report) — Muster: Reminder-Commands aus dem Einmalprodukte-Projekt.
- Kein Zwang, kein Big-Bang: nicht migrierte Abos laufen unbefristet über PAYONE.
---
## 12.1 Phase S9 (nachgelagert) – Rückbuchungen & Stornierungen erfassen
**Stakeholder-Entscheid 2026-07-09:** Als eigener Punkt aufgenommen, bewusst
**nachgelagert** (nach S4–S7). Zukünftig müssen Rückbuchungen und Stornierungen
systemseitig miterfasst werden:
| # | Aufgabe |
|---|---------|
| S9.1 | **Rückbuchungen (Chargebacks/Disputes):** `charge.dispute.created`/`charge.dispute.closed` verarbeiten — Zahlung/Bestellung kennzeichnen, `PaymentIncident` erzeugen (Überschneidung S6.2), Betrag inkl. Dispute-Gebühr ausweisen. PAYONE-Pendant (Rückläufer/`cancelation`) mitdenken. |
| S9.2 | **Erstattungen:** `charge.refunded` (Voll/Teil) an der Zahlung verbuchen; Verzahnung mit dem Widerrufs-/Refund-Flow aus S7.2. |
| S9.3 | **Stornierungen:** Stripe-seitige Cancels (`payment_intent.canceled`, abgebrochene/stornierte Vorgänge) sauber am Vorgang dokumentieren. |
| S9.4 | **Buchhaltung:** Klären, wie Rückbuchungen/Storni in Rechnungskorrektur/DATEV-Export einfließen (mit Stakeholder/Steuerberater). |
| S9.5 | Auswertung: Kacheln/Listen im Payment-Dashboard (Überschneidung S7.4). |
---
## 13. Teststrategie (übergreifend)
| Ebene | Inhalt |
@ -627,13 +878,27 @@ routes/api.php, routes/domains/*.php
Abo-/Order-Views (Provider-Badge, maskiertes Zahlungsmittel)
```
## 16. Nächste Schritte
## 16. Nächste Schritte (Stand 2026-07-09)
1. ~~O-Fragen klären~~ ✅ **beantwortet 2026-07-08** (Konzept §10): Hosted Checkout mit
Redirect, alle Stripe-Zahlarten inkl. PayPal/Apple Pay/Google Pay/Amazon Pay/Klarna,
nur EUR, SDK freigegeben, Account vorhanden, Bestands-Abo-Migration und
Membership/ELV bewusst nach hinten gestellt (S8 bzw. Folgeprojekt).
2. Phase **S0** starten: Test-Keys + Webhook im (vorhandenen) Stripe-Account einrichten,
`stripe/stripe-php` installieren, `config/stripe.php`, **N1-Prüfung** (Klarna/Amazon
Pay Abo-Fähigkeit).
3. Danach S1 → S2 → S3; nach S3 erster produktiver Meilenstein (Einmalzahlung) möglich.
1. ~~**Testmodus-E2E für S4/S5**~~ ✅ **komplett 2026-07-09**: Abo-Abschluss, Cron-Abbuchung
(inkl. `stripe:process-pending-events`), Zahlungsart-Wechsel (CRM + Kunden-Link,
PayPal, PAYONE→Stripe-Umzug), Decline → Status 3, Recovery-Checkout,
authentication_required. Aufräumen: Test-Abos #440/#442 (Abo 442 steht nach dem
letzten Test auf Status 3 mit der `…3184`-Testkarte) und Test-Bestellungen bereinigen.
2. **Deployment + Migrationen** (3 neue: Provider-/Stripe-Abo-Spalten,
`widen_clearingtype`), dann im Admin: **`abo-payments-frozen` aufheben** (§7.2),
Vorkasse wieder einblenden (`payment-vorkasse-hidden` aus). Ersten 04:00-Cron-Lauf
beobachten (holt eingefrorene PAYONE-Abbuchungen nach → großer Lauf!):
`abo_order.log`, `stripe.log`, `stripe_webhook_events`, Payment-Dashboard.
Der Mischlauf PAYONE+Stripe verifiziert sich dabei. Dazu im **Live**-Stripe-Dashboard
das Zahlarten-Set mit dem Testmodus abgleichen (S7.6 kommuniziert u. a. Link, EPS,
Bancontact — Test/Live sind getrennt konfiguriert!).
3. ~~Phase **S5**~~ ✅ umgesetzt + E2E (2026-07-09, Scope erweitert auf „Zahlungsart
ändern"): CRM-Pfad und Berater-Kunden-Link im Browser verprobt (inkl. PayPal und
PAYONE→Stripe-Umzug, Abo 440). Nur der **Portal-Pfad** ist ohne Sichttest
(identischer Code-Pfad, geringe Prio).
4. ~~**S7.6 Zahlarten-Kommunikation**~~ ✅ umgesetzt (2026-07-09, siehe Umsetzungsstatus
in §11): Logos + Benennung in Checkout, Footer (alle Shop-Seiten) und `/zahlungsarten`.
**Offen:** kurzer Sichttest im Browser (Footer, `/zahlungsarten`, Checkout).
5. **S7.4 Payment-Dashboard** für Stripe anpassen/optimieren (Stakeholder 2026-07-09).
6. Nachgelagert: **S9 Rückbuchungen & Stornierungen** (§12.1).