- Provider-Status/Uptime: Stripe-Kachel aktiviert; payment:check-uptime prüft zusätzlich api.stripe.com/v1/ (401 ohne Key = erreichbar) inkl. Auto-Incident - Zahlungen: Anbieter-Filter, Provider-Badges, Umsatz je Provider, Stripe-Dashboard-Deep-Links (ShoppingPayment/PaymentTransaction:: stripeDashboardUrl, pi_-Guard, testmodus-bewusst) - Rohe Transaktionen: Anbieter-Filter/-Spalte, Erfolgsraten je Anbieter; Bugfix: Stringable-Vergleich ließ den txaction-Filter immer mit leerem Wert greifen — Liste war dauerhaft leer - Neue Kachel „Abos je Anbieter" (Index + GF-Ansicht) mit Fehlerzählern und Stripe-Anteil-Fortschrittsbalken (Auslaufmodell PAYONE → Stripe ablesbar) - Incident-Kachel weist Stripe/PAYONE getrennt aus; Log-Viewer mit Kanal-Umschalter stripe/payone - Plan-Doku: S7.4-Umsetzungsstatus, Abo-Go-Live 2026-07-10 vermerkt Tests: PaymentDashboardProviderTest (9 neu), Dashboard-Suite 49 grün. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
936 lines
71 KiB
Markdown
936 lines
71 KiB
Markdown
# Entwicklungsplan: Stripe als Zahlungsanbieter (PAYONE bleibt als Option)
|
||
|
||
> **Status:** In Arbeit (S0–S5 ✓, S7.4 ✓, S7.6 ✓ — **alles live seit 2026-07-10 inkl. Abos**, Freeze aufgehoben, Vorkasse wieder sichtbar, läuft fehlerfrei; offen: S7.1/S7.2/S7.3/S7.5, S6-Rest, S9 → §16) · **Erstellt:** 2026-07-08 · **Zuletzt aktualisiert:** 2026-07-10
|
||
>
|
||
> **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/`
|
||
|
||
## Dokumentations-Hinweis (verbindlich)
|
||
|
||
Dieser Plan wird **phasenweise** abgearbeitet. Für **jede Phase** gilt:
|
||
|
||
- Der **Umsetzungsstatus** wird nach Abschluss auf `ERLEDIGT` gesetzt und sauber
|
||
dokumentiert (Datum, betroffene Dateien, kurze Begründung).
|
||
- Abweichungen, Detail-Entscheidungen und Verworfenes werden dokumentiert.
|
||
- Jede Änderung wird **programmatisch getestet** (Pest), bevor die Phase als erledigt gilt.
|
||
- Nach Code-Änderungen läuft `./vendor/bin/pint`.
|
||
|
||
---
|
||
|
||
## 1. Ziel
|
||
|
||
Stripe wird als **primäre Zahlungsabwicklung** eingeführt und ersetzt PAYONE im
|
||
Tagesgeschäft. Dabei gilt:
|
||
|
||
| # | Anforderung |
|
||
|---|-------------|
|
||
| A | Stripe übernimmt **nur die Zahlungsabwicklung**. Rechnungen, Lieferscheine, Provisionen, DATEV etc. bleiben im System. |
|
||
| B | **PAYONE wird nicht entfernt.** Es bleibt vollständig funktionsfähig, sauber vom Stripe-Code separiert. |
|
||
| C | **Schalter in den Einstellungen** wählt den aktiven Anbieter für **neue** Zahlungsvorgänge (`payone` ↔ `stripe`). Bestandszahlungen/-abos behalten ihren Anbieter. |
|
||
| D | Scope-Reihenfolge: **1. Einmalzahlungen**, **2. Abos** (Erstzahlung + wiederkehrende Abbuchung). |
|
||
| E | **Netto/Brutto je Land** wird weiterhin systemseitig berechnet; Stripe erhält nur den Bruttobetrag in Cent. |
|
||
|
||
**Kern-Architekturentscheidung** (Begründung in `STRIPE-KONZEPT.md` §2):
|
||
Stripe wird als reiner **Charge-/Token-Provider** genutzt — **PaymentIntents +
|
||
gespeicherte PaymentMethods**, *kein* Stripe Billing / Invoicing / Tax. Der Abo-Zyklus
|
||
(Cron `user:make_abo_order`, Betragsberechnung, Rechnungen) bleibt unverändert im System.
|
||
|
||
## 2. Zielarchitektur (Kurzfassung)
|
||
|
||
```
|
||
Checkout / Abo-Cron / Recovery / Widerruf
|
||
│
|
||
▼
|
||
PaymentProviderManager ← Einstellungs-Schalter (neu) + payment_provider-Spalte (Bestand)
|
||
│
|
||
├── PayoneGateway → Adapter um bestehenden PayonePaymentService (unverändert)
|
||
└── StripeGateway → app/Services/Payment/Stripe/* (neu)
|
||
├── StripeClientWrapper (injizierbar, testbar)
|
||
├── StripePaymentService (PaymentIntents, SetupIntents, Refunds)
|
||
├── StripeWebhookHandler (+ DTOs)
|
||
└── StripeTransactionRecorder
|
||
```
|
||
|
||
Neue DB-Felder: `payment_provider` auf `user_abos`/`shopping_payments`/`payment_transactions`,
|
||
`stripe_customer_id`/`stripe_payment_method_id` auf `user_abos`, `stripe_customer_id` am
|
||
User/Customer, `external_reference` auf `shopping_payments`. Details: Konzept §4.3.
|
||
|
||
## 3. Phasen-Übersicht
|
||
|
||
| Phase | Titel | Aufwand (Schätzung) | Priorität | Abhängigkeiten |
|
||
|-------|-------|---------------------|-----------|----------------|
|
||
| **S0** | Grundlagen: Account, SDK, Config, Entscheidungen | 1–2 PT | Blocker | – (O-Fragen beantwortet 2026-07-08) |
|
||
| **S1** | Provider-Abstraktion, Datenmodell & Einstellungs-Schalter | 4–6 PT | P0 | S0 |
|
||
| **S2** | Stripe-Basis: Client, Recorder, Webhook-Endpoint | 4–6 PT | P0 | S1 |
|
||
| **S3** | Einmalzahlung im Checkout über Stripe | 6–10 PT | **P1** | S2 |
|
||
| **S4** | Abos: Erstzahlung + wiederkehrende Abbuchung | 8–12 PT | **P1** | S3 |
|
||
| **S5** | Zahlungsmittel-Update über Stripe (SetupIntent) | 3–5 PT | P2 | S4 |
|
||
| **S6** | Fehlerfälle, 3DS-Recovery & Incident-Anbindung | 4–6 PT | P1–P2 | S4 |
|
||
| **S7** | Admin & Betrieb: Schalter-UI, Refunds, Monitoring | 4–6 PT | P2 | S3 (Refunds), S1 (UI) |
|
||
| **S8** | Rollout & optionale Abo-Migration PAYONE → Stripe | 3–5 PT | P3 | S4–S7 |
|
||
|
||
*PT = Personentage (1 Entwickler). Einmalzahlung produktiv nach S3+S7-Teilen; Abos nach S4+S6.*
|
||
|
||
---
|
||
|
||
## 4. Phase S0 – Grundlagen · Status: **WEITGEHEND ERLEDIGT** (2026-07-08)
|
||
|
||
### Ziel
|
||
Technische und organisatorische Voraussetzungen schaffen; keine Feature-Logik.
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe | Status |
|
||
|---|---------|--------|
|
||
| S0.1 | Offene Fragen O1–O9 klären (Konzept §10); N2/N3 ebenfalls beantwortet. | ✅ 2026-07-08 · **N1 offen** (Klarna/Amazon Pay off_session gegen aktuelle Stripe-Doku prüfen, spätestens zu S4-Beginn) |
|
||
| S0.2 | Stripe-Account (vorhanden, Stakeholder verwaltet): Test-Keys beschaffen, Webhook-Endpoint (Test) konfigurieren, gewünschte Zahlarten im Dashboard aktivieren (Karte, PayPal, Apple/Google Pay, Amazon Pay, Klarna, …). | ✅ 2026-07-08 — Test-Keys in `.env`, per Balance-API verifiziert (`livemode: false`). Webhook auf dem Test-Server via **Stripe CLI** (`stripe listen --forward-to … --skip-verify`, CLI-Signing-Secret als `STRIPE_WEBHOOK_SECRET`); Dashboard-Endpoint + eigenes `whsec` erst für Produktion nötig (Anleitung §6). Zahlarten-Aktivierung im Dashboard: vor S3-Manualtest prüfen. |
|
||
| S0.3 | Composer: `stripe/stripe-php` installieren (Freigabe O6). | ✅ 2026-07-08 — `stripe/stripe-php` **^20.3** in `composer.json`/`composer.lock` |
|
||
| S0.4 | `config/stripe.php` anlegen (`secret`, `key`, `webhook_secret`, `mode`); `.env.example`, `.env`, `phpunit.xml` ergänzt. | ✅ 2026-07-08 — **Abweichung s. u.** |
|
||
| S0.5 | `PaymentProvider`-Enum anlegen. | ✅ 2026-07-08 — `app/Enums/PaymentProvider.php` (`Payone`/`Stripe`, string-backed) |
|
||
|
||
### Abweichungen / Entscheidungen
|
||
|
||
- **Kein Boot-Hard-Fail in `config/stripe.php`** (Plan sah das Muster aus
|
||
`config/payone.php` vor): PAYONE ist produktiv im Einsatz — dort ist ein fehlender Key
|
||
ein Fehler. Stripe dagegen wird per **Dark Launch** deployt (Rollout-Schritt 2, §12);
|
||
ein Boot-Hard-Fail würde jedes Deployment ohne gesetzte Stripe-Keys sofort brechen.
|
||
Stattdessen: Config liest `env()` tolerant, der **`StripeClientWrapper` (Phase S2)
|
||
schlägt beim ersten Zugriff hart fehl**, wenn `stripe.secret` fehlt. Begründung als
|
||
Kommentar in `config/stripe.php` dokumentiert.
|
||
- Enum liegt unter `app/Enums/` (neu, via `make:enum`) — im Plan §15 so vorgesehen.
|
||
|
||
### Tests & Verifikation (2026-07-08)
|
||
|
||
- `tests/Feature/StripeFoundationTest.php` *(neu)*: Config-Werte aus Env, kein
|
||
Boot-Fail bei fehlenden Secrets, Enum-Werte/`from`/`tryFrom`, SDK-Klasse vorhanden —
|
||
**4 Tests, 11 Assertions, grün**.
|
||
- PAYONE-Regressionssuite (PayoneTest, PaymentMethodResolverTest,
|
||
PayonePaymentServiceTest, PayoneWebhookHandlerTest, PayoneTransactionRecorderTest,
|
||
PayoneTransportExceptionHandlingTest): **45 passed, 1 skipped (optionaler
|
||
Netzwerktest), 209 Assertions** — unverändert grün.
|
||
- `./vendor/bin/pint --dirty` grün.
|
||
|
||
### Akzeptanzkriterien
|
||
- [x] SDK installiert, `composer.lock` aktualisiert, Test-Suite weiterhin grün.
|
||
- [x] Keys nur via `.env`; kein Secret im Repo. (Hard-Fail bewusst in S2 verschoben, s. Abweichung.)
|
||
- [x] Alle O-Fragen beantwortet oder explizit vertagt (N1 mit Vermerk offen).
|
||
- [ ] Test-Keys + Webhook-Secret in `.env` eingetragen (S0.2, Stakeholder).
|
||
|
||
---
|
||
|
||
## 5. Phase S1 – Provider-Abstraktion, Datenmodell & Schalter · Status: **ERLEDIGT** (2026-07-08)
|
||
|
||
### Ziel
|
||
Das System kennt zwei Anbieter. Jede Zahlung/jedes Abo trägt seinen Anbieter fest;
|
||
ein Einstellungs-Schalter bestimmt den Anbieter für **neue** Vorgänge. PAYONE-Verhalten
|
||
ändert sich **nicht** (Default `payone`, Schalter steht auf `payone`).
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe | Status |
|
||
|---|---------|--------|
|
||
| S1.1 | Migrationen: `payment_provider` auf `user_abos`, `shopping_payments`, `payment_transactions` (Default `'payone'`); `stripe_customer_id` + `stripe_payment_method_id` auf `user_abos`; `external_reference` auf `shopping_payments`; `stripe_customer_id` an `users`/`customers`. | ✅ `2026_07_08_140000_add_payment_provider_columns.php` (idempotent mit `hasColumn`-Guards, Haus-Stil) |
|
||
| S1.2 | `PaymentGatewayInterface` + DTOs definieren. | ✅ `Contracts/PaymentGatewayInterface.php`, `DTOs/AboChargeResult.php` — **Abweichung s. u.** |
|
||
| S1.3 | `PayoneGateway` als Adapter um `PayonePaymentService` — **kein Umbau am PAYONE-Code**. | ✅ `app/Services/Payment/PayoneGateway.php`; `PayonePaymentService` unverändert |
|
||
| S1.4 | `PaymentProviderManager`: `providerForNewCheckout()`, `providerFor()`, `gateway()`. | ✅ `app/Services/Payment/PaymentProviderManager.php` |
|
||
| S1.5 | Einstellungs-Schalter als persistente Admin-Einstellung. | ✅ Bestehender Settings-Mechanismus (`Setting::get/setContentBySlug`), Slug **`payment-active-provider`** (Typ `text`); fehlender/ungültiger Wert → Fallback `payone`. Admin-UI + `SysLog` folgen in **S7.1**. |
|
||
| S1.6 | Abo-Cron/Retry lesen den Provider **des Abos** (nicht den Schalter). | ✅ Guard in `UserMakeOrder::makePayment()`: Nicht-PAYONE-Provider → Log + `RuntimeException` **vor** jedem PAYONE-Aufruf. Deckt auch `AboRetryPaymentService` ab (delegiert an `UserMakeOrder`). Echte Stripe-Verzweigung kommt in S4.4. |
|
||
|
||
### Abweichungen / Entscheidungen
|
||
|
||
- **Interface wächst phasenweise:** Deklariert sind nur `provider()` und `chargeAbo()`
|
||
(einziger realer Aufrufer heute). `initiateCheckoutPayment()` (S3),
|
||
`setupPaymentMethodUpdate()` (S5) und `refund()` (S7) werden erst mit ihrem Aufrufer
|
||
ergänzt — kein toter Vertrag, kein Zwang zu Dummy-Implementierungen im Adapter.
|
||
Im Interface-Docblock dokumentiert.
|
||
- **`payment_provider` bleibt String-Spalte ohne Enum-Cast** am Model — der
|
||
`PaymentProviderManager` normalisiert via `PaymentProvider::tryFrom()` mit
|
||
PAYONE-Fallback. So können keine bestehenden Blade-/Array-Zugriffe brechen.
|
||
- **Schalter-Ablage:** bestehender `settings`-Mechanismus statt eigener Tabelle
|
||
(gleiches Muster wie `abo-lock-days-change`, `abo-card-update-link-ttl-hours`).
|
||
- `PaymentProviderManager::gateway(Stripe)` wirft bewusst `RuntimeException`, bis das
|
||
Stripe-Gateway existiert (S2/S4) — Fail-fast statt stillem Fallback auf PAYONE.
|
||
- Models: `payment_provider`/`stripe_*`/`external_reference`/`provider` in die
|
||
`$fillable` von `UserAbo`, `ShoppingPayment`, `PaymentTransaction` aufgenommen.
|
||
|
||
### Tests & Verifikation (2026-07-08)
|
||
|
||
- `tests/Feature/PaymentProviderManagerTest.php` *(neu)*: **12 Tests, 25 Assertions** —
|
||
Schalter-Default/`stripe`/ungültiger Wert, Backfill-Default `payone`, Spalte schlägt
|
||
Schalter, Gateway-Auflösung (PayoneGateway / Stripe-Exception),
|
||
`AboChargeResult`-Mapping (APPROVED/ERROR), Gateway-Delegation (gemockter
|
||
`PayonePaymentService`), Cron-Guard blockt Stripe-Abo.
|
||
- Regression: PAYONE-Suite + `StripeFoundationTest` (**49 passed, 1 skipped, 220
|
||
Assertions**) und Abo-Retry/Recovery-Suite (`AboPendingInitialPaymentTest`,
|
||
`AboRecoveryAdvanceTest`, `AboUserRetryTest`, `AdminAboRetryPaymentTest`,
|
||
`RetryFailedPaypalAbosTest`: **21 passed, 56 Assertions**) — unverändert grün.
|
||
- `./vendor/bin/pint --dirty` grün (nur Format-Fixes in `ShoppingPayment`).
|
||
|
||
### Akzeptanzkriterien
|
||
- [x] Schalter existiert, Default `payone`, Systemverhalten identisch zu vorher.
|
||
- [x] Kein PAYONE-Service wurde inhaltlich geändert (nur Adapter obendrauf; einzige Bestandsänderung: defensiver Provider-Guard im Cron + `$fillable`-Erweiterungen).
|
||
|
||
---
|
||
|
||
## 6. Phase S2 – Stripe-Basisdienste · Status: **ERLEDIGT** (2026-07-08)
|
||
|
||
### Ziel
|
||
Stripe technisch angebunden: Client, Transaktions-Protokollierung, Webhook-Empfang —
|
||
noch ohne Checkout-UI.
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe | Status |
|
||
|---|---------|--------|
|
||
| S2.1 | `StripeClientWrapper` — injizierbare Hülle um `\Stripe\StripeClient`; Singleton im `AppServiceProvider`. | ✅ Lazy-Init; **Hard-Fail bei Nutzung ohne `STRIPE_SECRET`** (der aus S0 verschobene Fail-fast); `isConfigured()` für Dark-Launch-Checks |
|
||
| S2.2 | `StripeTransactionRecorder` — einzige Stelle für `PaymentTransaction` mit `provider='stripe'`; Redaction. | ✅ `recordFromStripeObject()`; `redactPayload()` entfernt `client_secret` **rekursiv** (Stripe-Payloads enthalten kein PAN — sensibel ist das Intent-Secret) |
|
||
| S2.3 | `StripeCustomerService` — Customer idempotent anlegen, ID persistieren. | ✅ `customerIdFor(User\|Customer)`; vorhandene ID wird nie überschrieben; Metadata `mivita_type`/`mivita_id` |
|
||
| S2.4 | Webhook: Route, schlanker Controller, Handler mit Signaturprüfung + Idempotenz. | ✅ `POST /stripe/webhook` (`routes/api.php`, läuft unter `api.<domain>`); `Api\StripeWebhookController` (Muster R5); `StripeWebhookHandler` + `StripeWebhookResult`-DTO |
|
||
| S2.5 | Logging-Kanal `stripe.log`. | ✅ `config/logging.php`, daily, Level `info` (bewusst gesprächiger als `payone` — Rollout-Beobachtung) |
|
||
|
||
### Abweichungen / Entscheidungen
|
||
|
||
- **Idempotenz über eigene Tabelle `stripe_webhook_events`** (`event_id` unique, Payload
|
||
redaktiert als Audit, `processed_at`): Migration
|
||
`2026_07_08_150000_create_stripe_webhook_events_table.php` + Model
|
||
`StripeWebhookEvent`. Race-sicher: Exists-Check + Unique-Constraint-Catch
|
||
(paralleler Stripe-Retry → `duplicate`).
|
||
- **HTTP-Statuscodes statt PAYONE-„immer TSOK":** ungültige Signatur → `400` (kein
|
||
Retry sinnvoll); fehlendes `STRIPE_WEBHOOK_SECRET` → `500` (Stripe stellt später
|
||
erneut zu); Duplikat/unbehandelter Typ/verarbeitet → `200`. Im DTO dokumentiert.
|
||
- **Zahlungs-Zuordnung** im Handler: `metadata.shopping_payment_id` (wird ab S3 gesetzt)
|
||
oder `external_reference` (PaymentIntent-/Session-ID); ohne Treffer wird das Event
|
||
trotzdem gespeichert und bestätigt (Business-Verdrahtung folgt in S3/S4 an
|
||
`dispatchEvent()`).
|
||
- Behandelte Event-Typen als `HANDLED_TYPES`-Konstante gemäß Konzept §7 (inkl.
|
||
`checkout.session.completed`/`expired`); unbekannte Typen werden protokolliert + 200.
|
||
|
||
### Tests & Verifikation (2026-07-08)
|
||
|
||
- `tests/Feature/StripeWebhookHandlerTest.php` *(neu)*: **11 Tests, 35 Assertions** —
|
||
ungültige Signatur (400, nichts gespeichert), fehlendes Secret (500), Duplikat
|
||
(200, eine Zeile), unbehandelter Typ (gespeichert + 200), Transaktion via Metadata
|
||
(provider `stripe`, `client_secret` redaktiert), Auflösung via `external_reference`
|
||
+ `processed_at`, rekursive Redaction, Wrapper-Hard-Fail ohne Secret, Lazy-Client,
|
||
CustomerService (vorhandene ID ohne API-Call; Create persistiert ID — Stripe-Client
|
||
als Partial-Mock auf `request`, kein echter HTTP-Call).
|
||
- Handler wird wie beim PAYONE-Pendant **direkt** getestet (HTTP-Routen sind in Pest
|
||
nicht domain-aware); Route via `php artisan route:list` verifiziert:
|
||
`POST api.mivita.test/stripe/webhook → api.stripe_webhook`.
|
||
- Regression: `PaymentProviderManagerTest` + `StripeFoundationTest` +
|
||
`PayoneWebhookHandlerTest` + `PayoneTransactionRecorderTest`: **35 passed,
|
||
131 Assertions**. `pint --dirty` grün.
|
||
|
||
### Akzeptanzkriterien
|
||
- [x] `PaymentTransaction`-Schreibzugriffe für Stripe existieren **nur** im Recorder.
|
||
- [x] Webhook idempotent, signaturgeprüft, protokolliert (per Test nachgewiesen).
|
||
- [x] Ende-zu-Ende gegen das **laufende System** verifiziert *(2026-07-08)*: signierter
|
||
`payment_intent.succeeded`-Request an `api.mivita.test/stripe/webhook` (echte
|
||
Signaturprüfung mit dem `whsec` aus `.env`) → `200 processed`, Event-Zeile mit
|
||
`processed_at`; erneuter identischer Request → `200 duplicate` (weiterhin 1 Zeile);
|
||
manipulierte Signatur → `400 invalid_signature`. Migrationen auf der
|
||
Dev-MySQL ausgeführt (`add_payment_provider_columns`, `stripe_webhook_events`).
|
||
Test-Keys per Balance-API bestätigt (`livemode: false`).
|
||
- [x] **Echte Stripe-Events über `stripe listen` verifiziert** *(2026-07-08)*: Testmodus-
|
||
PaymentIntent (API, `pm_card_visa`) → Listener → Endpoint. `payment_intent.succeeded`
|
||
signaturgeprüft verarbeitet (`processed_at` gesetzt); Begleit-Events
|
||
(`payment_intent.created`, `charge.succeeded`, `charge.updated`) korrekt als
|
||
unbehandelte Typen gespeichert + bestätigt. Stolpersteine dabei (für die Nachwelt):
|
||
1. Listener lief zunächst mit dem API-Key eines **anderen** Stripe-Accounts,
|
||
2. `api.mivita.test` fehlte in der `/etc/hosts` des Hosts,
|
||
3. `whsec` muss vom **mivita-Listener** stammen (CLI-Secret ist account-gebunden).
|
||
|
||
### Webhook-Einrichtung (S0.2)
|
||
|
||
**Test-Server (aktuell im Einsatz):** Stripe CLI auf dem Host — kein Dashboard-Endpoint nötig:
|
||
|
||
```bash
|
||
stripe listen --forward-to https://api.mivita.test/stripe/webhook --skip-verify
|
||
```
|
||
|
||
Das von `stripe listen` ausgegebene CLI-Signing-Secret (`whsec_…`) als
|
||
`STRIPE_WEBHOOK_SECRET` in die `.env`. `--skip-verify`
|
||
wegen des selbstsignierten `.test`-Zertifikats. Events auslösen:
|
||
`stripe trigger payment_intent.succeeded` (bzw. echte Testmodus-Checkouts ab S3 —
|
||
`stripe listen` leitet standardmäßig alle Events weiter, unser Handler bestätigt
|
||
unbehandelte Typen mit `200 ignored`).
|
||
|
||
> ⚠️ **Mehrere Apps / Accounts auf demselben Test-Server:** Das CLI-Signing-Secret
|
||
> gehört zum **Stripe-Account des Listeners**, nicht zur Forward-URL. Ein für eine
|
||
> andere App (anderer Account, z. B. pressekonto) laufender Listener empfängt keine
|
||
> mivita-Events, und sein `whsec` passt nicht zu unserer Signaturprüfung. Für mivita
|
||
> daher einen **eigenen Listener** parallel starten — einfachster Weg ohne Re-Login:
|
||
>
|
||
> ```bash
|
||
> stripe listen --api-key sk_test_…(mivita) \
|
||
> --forward-to https://api.mivita.test/stripe/webhook --skip-verify
|
||
> ```
|
||
>
|
||
> Das dabei ausgegebene `whsec_…` in die mivita-`.env`, dann `php artisan config:clear`.
|
||
> Testen: `stripe trigger payment_intent.succeeded --api-key sk_test_…(mivita)`.
|
||
> (Stand 2026-07-08: das zuerst eingetragene `whsec` stammte vom pressekonto-Listener
|
||
> und muss durch das mivita-Listener-Secret ersetzt werden.)
|
||
|
||
**Produktion (später, vor Rollout-Schritt 2):**
|
||
|
||
1. Dashboard → Developers → Webhooks → **Add endpoint**.
|
||
2. **Endpoint-URL:** `https://api.mivita.care/stripe/webhook`.
|
||
3. **Events auswählen:** `checkout.session.completed`, `checkout.session.expired`,
|
||
`payment_intent.succeeded`, `payment_intent.processing`,
|
||
`payment_intent.payment_failed`, `charge.succeeded` *(Zahlart-Anzeige)*,
|
||
`charge.refunded`, `charge.dispute.created`, `setup_intent.succeeded`.
|
||
4. Das **Endpoint-Signing-Secret** (`whsec_…`, ein anderes als das CLI-Secret!) als
|
||
`STRIPE_WEBHOOK_SECRET` in die Produktions-`.env`.
|
||
|
||
---
|
||
|
||
## 7. Phase S3 – Einmalzahlung im Checkout ⭐ · Status: **ERLEDIGT & LIVE** (2026-07-09)
|
||
|
||
### Ziel
|
||
Steht der Schalter auf `stripe`, laufen **neue Einmalzahlungen** (Shop, Zahlungslinks)
|
||
über die **Stripe-Hosted-Checkout-Seite** (Redirect-Flow, Entscheidung 2026-07-08,
|
||
Konzept §3.1): Zahlartenwahl und Dateneingabe finden komplett bei Stripe statt.
|
||
PAYONE-Checkout bleibt bei Schalter `payone` unverändert.
|
||
|
||
### Ablauf (Soll)
|
||
|
||
```
|
||
mivita-Checkout (Adresse, Warenkorb, AGB — unverändert)
|
||
→ „Jetzt zahlen" → CheckoutController
|
||
→ PaymentProviderManager::providerForNewCheckout()
|
||
├── payone → bestehender Pfad inkl. Zahlartenwahl (unverändert)
|
||
└── stripe → StripeGateway::initiateCheckoutPayment()
|
||
├── ShoppingPayment anlegen (provider=stripe, mode, Betrag brutto)
|
||
├── Checkout Session erstellen (mode=payment, amount=Cent, EUR,
|
||
│ customer, metadata: order/payment/mode, success_url, cancel_url;
|
||
│ Zahlarten dynamisch via Stripe-Dashboard)
|
||
└── 303 Redirect → checkout.stripe.com
|
||
Kunde wählt bei Stripe die Zahlart (Karte/PayPal/Apple Pay/Google Pay/Amazon Pay/Klarna/…),
|
||
zahlt (inkl. 3DS) und wird zurückgeleitet:
|
||
├── success_url → Bestätigungsseite (Status nur zur UX-Anzeige)
|
||
└── cancel_url → zurück in den mivita-Checkout (Warenkorb bleibt erhalten)
|
||
Webhook checkout.session.completed / payment_intent.succeeded
|
||
→ paymentStatusPaidAction-Pfad (Order bezahlt, Mails, Incentive) — einzige Wahrheit
|
||
```
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe |
|
||
|---|---------|
|
||
| S3.1 | Checkout-Verzweigung nach Provider in `CheckoutController`; bei `stripe` **entfällt die Zahlartenwahl** — nach Checkout-Prüfung direkt zu Stripe (N2, entschieden 2026-07-08). Ausnahme: ist der Vorkasse-Schalter (S3.8) aktiv bzw. FNC freigeschaltet, wird eine reduzierte Wahl „Online bezahlen / Vorkasse (/ Rechnung)" angezeigt. Dabei die im Alt-Plan aufgeschobene Extraktion des Checkout-HTTP-Flows aus `Pay\PayoneController` **nur soweit nötig** vorziehen (keine PAYONE-Verhaltensänderung). |
|
||
| S3.2 | `StripePaymentService::initiateCheckoutPayment()`: Checkout Session (`mode=payment`) erstellen. Betrag: bestehende Cart-Brutto-Summe, Rundung auf Cent an **einer** Stelle; Währung fest EUR; `line_items` aus dem Warenkorb (Anzeige auf der Stripe-Seite), Metadata mit Order-/Payment-Referenz; Session-ID in `shopping_payments.external_reference`. |
|
||
| S3.3 | Redirect-Handling: 303 auf die Session-URL; `success_url`/`cancel_url` domain-korrekt generieren (Multi-Tenant beachten, `routes/domains/…`). `cancel_url` führt zurück in den Checkout, Warenkorb/ShoppingInstance bleiben nutzbar. |
|
||
| S3.4 | Success-/Cancel-Controller: liest Session-/PaymentIntent-Status nur für UX (`paid`/`processing`/abgebrochen), verändert **keine** Bezahlt-Flags. Abgelaufene Sessions (`checkout.session.expired`) räumen den offenen Vorgang auf. |
|
||
| S3.5 | Webhook-Paid-Pfad: `checkout.session.completed` + `payment_intent.succeeded` → gemeinsame, provider-neutrale Paid-Verarbeitung (Wiederverwendung `Payment::paymentStatusPaidAction()`-Logik; ggf. Extraktion in provider-neutralen Service). Preis-/Betragsabgleich wie beim PAYONE-Webhook (Guard 2005-Äquivalent). |
|
||
| S3.6 | `processing`-Status (SEPA & Co.) im Bestell-/Zahlungsstatus sichtbar machen (Muster „Zahlung in Verarbeitung" aus Alt-Plan §5.2.1). |
|
||
| S3.7 | Zahlungslinks (`OrderPaymentService`, `ShoppingInstance`) provider-fähig machen: neuer Link erzeugt bei Schalter `stripe` direkt eine Checkout Session (der Link **ist** dann faktisch die Stripe-Seite mit unserem Vor-/Nachlauf). |
|
||
| S3.8 | **Vorkasse-Sichtbarkeits-Schalter** (N3, entschieden 2026-07-08): eigene Einstellung, um Vorkasse (`vor`, läuft über PAYONE) unabhängig vom Provider-Schalter aus-/einzublenden — Vorkasse bleibt technisch erhalten. Interne Rechnung (FNC) bleibt wie heute über `payment_methods.active` steuerbar (aktuell praktisch überall deaktiviert). |
|
||
|
||
### Tests
|
||
- Feature: Checkout-Verzweigung je Schalterstellung; Session-Parameter (Betrag = Cart-Brutto in Cent, EUR, Metadata, success/cancel-URLs, Abo-Einschränkung greift hier nicht) gegen gemockten Client.
|
||
- Webhook-Paid: Order wird bezahlt, Mails/Incentive-Pfad läuft, Duplikat-Event ändert nichts, Betrags-Mismatch wird abgewiesen + Incident; `session.expired` räumt auf.
|
||
- Cancel-Pfad: Rücksprung lässt Warenkorb intakt, kein Zahlungs-/Order-Nebeneffekt.
|
||
- Netto/Brutto: Testfälle mit mind. 2 Ländern/Steuersätzen (Cart-Brutto ↔ Rechnungs-Netto unverändert zu PAYONE-Pfad).
|
||
- Regression: kompletter PAYONE-Checkout-Testbestand grün bei Schalter `payone`.
|
||
|
||
### Umsetzungsstatus & Abweichungen (2026-07-08)
|
||
|
||
| # | Status |
|
||
|---|--------|
|
||
| S3.1 | ✅ `CheckoutController`: `stripeCheckoutActive()` (Schalter, **Abos/Recovery bleiben bis S4 auf PAYONE**); View zeigt bei Stripe eine einzige „Online-Zahlung"-Option + VOR/FNC (sofern aktiv); `checkoutFinal` validiert `payment_method=stripe` gegen den Schalter (sonst 404). Keine PAYONE-Extraktion nötig — Verzweigung liegt **vor** dem PAYONE-Pfad. |
|
||
| S3.2 | ✅ `StripePaymentService::initiateCheckoutPayment()`. **Abweichung:** ein einzelnes `line_item` mit dem Gesamt-Bruttobetrag statt Warenkorb-Positionen — eliminiert jedes Rundungs-/Summenabweichungsrisiko zwischen Cart und Stripe; Positionsdetails stehen auf der Systemrechnung. Metadata (`shopping_order_id`, `shopping_payment_id`, `reference`, `mode`) auf Session **und** PaymentIntent. |
|
||
| S3.3 | ✅ Redirect via `redirect()->away($session->url)`; Session-ID → `external_reference`; Initiations-Transaktion `checkout.session.created` protokolliert (Bestätigungsseite hat dadurch immer eine `pay_trans`). |
|
||
| S3.4 | ✅ **Abweichung:** kein neuer Return-Controller — `success_url`/`cancel_url` nutzen die bestehenden `checkout.transaction_status`-Routen (Status-UX, Cart-Destroy, Fehlerseiten inkl. Cancel-Rücksprung wie bei PAYONE-Redirects). `checkout.session.expired` räumt serverseitig auf (Status `expired`, Instanz `link_canceled`). |
|
||
| S3.5 | ✅ `StripeWebhookHandler::dispatchEvent()` → gleiche txaction-Taxonomie wie PAYONE (`appointed`/`pending`/`failed`/`paid` + Prioritäten): `session.completed(paid)` & `payment_intent.succeeded` → Paid-Pfad (`Payment::paymentStatusPaidAction`, DB-Lock, Betrags-Guard analog 2006, Mail einmalig); `session.completed(unpaid)` → `appointed` + Bestellbestätigung; Reprocessing: gespeichertes, aber unverarbeitetes Event (Crash → Stripe-Retry) wird erneut verarbeitet statt als Duplikat verworfen. |
|
||
| S3.6 | ✅ `payment_intent.processing` → txaction `pending` — vorhandene Statusanzeigen greifen durch die gemeinsame Taxonomie automatisch. **Keine Mail** (Bestellbestätigung ging bei `appointed` raus). |
|
||
| S3.7 | ✅ implizit: Zahlungslinks führen in denselben Checkout → Verzweigung greift automatisch. Beim Browser-Manualtest mit verifizieren. |
|
||
| S3.8 | ✅ Vorkasse-Schalter: Setting **`payment-vorkasse-hidden`** (bool; fehlend/false = sichtbar) → `PaymentProviderManager::isVorkasseVisible()`, Filter in `CheckoutRepository::getPaymentsMethods()` (wirkt provider-unabhängig, auch im PAYONE-Betrieb). Admin-UI folgt in S7.1. |
|
||
|
||
**Ergänzung Zahlart-Anzeige (2026-07-08):** `charge.succeeded` wird jetzt verarbeitet —
|
||
die `payment_method_details` (Typ, Kartenmarke, letzte 4, Apple/Google-Pay-Wallet, Ablauf)
|
||
werden in `shopping_payments.carddata` gespeichert; `ShoppingPayment::getPaymentType()`
|
||
zeigt daraus z. B. „Apple Pay (Amex •••• 2003)", „Kreditkarte (Visa •••• 4242)", „PayPal",
|
||
„Klarna". Reihenfolge-sicher ohne API-Call: Kommt das Charge-Event vor dem
|
||
PaymentIntent-Event, zieht `markPaid()` die Details aus dem Event-Store nach
|
||
(`backfillPaymentMethodDetails`). Die PaymentIntent-ID wird dabei als
|
||
`external_reference` an der Zahlung gemerkt. Checkout-UI-Feinschliff nach
|
||
Stakeholder-Feedback: Radio heißt „Online-Zahlung", Stripe-Logo
|
||
(`payments-assets/stripe.png`), darunter Zahlarten-Zeile + Hinweistext.
|
||
|
||
**Weitere Entscheidung:** `payment_intent.payment_failed` verschickt bewusst **keine** Mail —
|
||
auf der Hosted Session kann der Kunde direkt erneut zahlen; jeder Fehlversuch feuert das
|
||
Event (Mail pro Versuch wäre Spam). `ShoppingPayment::getPaymentType()` zeigt für
|
||
Stripe-Zahlungen „Online-Zahlung (Stripe)"; neue Lang-Keys `payment.stripe_*` (de/en, du-Form).
|
||
|
||
**Tests (2026-07-08):** `tests/Feature/StripeCheckoutTest.php` *(neu)*: **9 Tests** —
|
||
Session-Parameter/Payment/Initiations-Transaktion (gemockter Client), Paid-Pfad + genau
|
||
eine Mail, kein Doppel-Mail bei zweitem Success-Event, Betrags-Mismatch verweigert,
|
||
`appointed` + Bestellbestätigung (async), `failed` ohne Mail, `expired`, Crash-Retry-
|
||
Reprocessing, Vorkasse-Schalter. Gesamt Stripe-Suite **36 Tests, 102 Assertions** grün;
|
||
Payment-Regression (PAYONE + Abo-Retry/Recovery, 66 Tests) grün; Blade kompiliert; Pint grün.
|
||
*(Hinweis: die volle Suite hat eine vorbestehende `makeUser()`-Redeclare-Kollision,
|
||
unabhängig von S3.)*
|
||
|
||
### Akzeptanzkriterien
|
||
- [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).
|
||
|
||
---
|
||
|
||
## 7.1 Notfall-Go-Live (2026-07-08): PAYONE-Ausfall — Einmalzahlungen live, Abos eingefroren
|
||
|
||
**Lage:** PAYONE ist ausgefallen. Entscheidung Stakeholder: Der S3-Stand
|
||
(Einmalzahlungen über Stripe) geht **sofort produktiv**; Phase S4 (Abos) wird
|
||
nachgelagert. Bis dahin werden **alle Abo-Zahlungen eingefroren** und mit einem
|
||
Hinweis versehen („derzeit nicht möglich, wir arbeiten an einer technischen Lösung").
|
||
|
||
### Not-Schalter `abo-payments-frozen` (Setting, bool)
|
||
|
||
| Eingefroren | Verhalten |
|
||
|---|---|
|
||
| Abo-Checkout (Neuabschluss) & Recovery-Checkout | Zahlarten-Box + Kaufen-Button ersetzt durch Warnhinweis (`payment.abo_payments_frozen`); Server-Guard in `checkoutFinal` blockiert Submits zusätzlich. |
|
||
| Abbuchungs-Cron `user:make_abo_order` | Kompletter Lauf wird übersprungen (Log-Warnung). Abos werden **nicht** als fehlgeschlagen markiert, `next_date` bleibt stehen → nach dem Auftauen holt der Cron alles regulär nach. |
|
||
| Abo-Retry (Admin/CRM/Portal, `AboRetryPaymentService`) | Liefert Fehler mit dem Hinweistext. |
|
||
| **Kreditkarte ändern** (Kunden-Link, Portal, CRM) | Zentraler Guard in `AboPaymentMethodService::updateCreditCard()` → `CardUpdateResult::error` mit Hinweistext; das gemeinsame Formular-Partial (`portal/abo/_payment_method_form`) zeigt statt der Kartenfelder die Warnbox; **Versand neuer Card-Update-Links** (`User\AboController::sendCardUpdateLink`) ist geblockt (Flash-Hinweis), damit keine Kunden-Mails auf ein eingefrorenes Formular führen. |
|
||
| Abschluss neuer Abos | Bereits über den Abo-Checkout-Guard abgedeckt (Hinweis + Server-Block, s. o.). |
|
||
|
||
Nicht eingefroren: Einmalzahlungen (Stripe), Vorkasse-/FNC-Sichtbarkeit (eigene
|
||
Schalter), CC-Update-Flow (schlägt bei PAYONE-Ausfall mit sauberer Fehlermeldung fehl).
|
||
Der Freeze wird nach S4-Abschluss aufgehoben (`Setting → false`); danach zahlen
|
||
Bestands-PAYONE-Abos wieder über PAYONE (sofern wiederhergestellt) bzw. neue über Stripe.
|
||
|
||
### Checkout-Wartungsmodus mit Admin-Bypass (`checkout-maintenance`, bool)
|
||
|
||
Für den kontrollierten Live-Start: Ist der Schalter an, sehen **alle Besucher** im
|
||
Checkout nur „Technische Schwierigkeiten — wir sind in Kürze wieder für dich da"
|
||
(`payment.checkout_maintenance`); **Admins** (`User::isAdmin()`, Session gilt
|
||
domänenübergreifend) können normal bestellen. So lässt sich Stripe im Live-Modus mit
|
||
einer echten Bestellung verproben, bevor der Checkout für alle öffnet. Guards in
|
||
`CheckoutController::checkout()` (Meldungsseite) und `checkoutFinal()` (Redirect).
|
||
|
||
### Admin-Settings-Karte „Zahlungen (Stripe / PAYONE)" (S7.1 vorgezogen)
|
||
|
||
Unter `https://my.mivita.care/admin/settings` (Karte nach „Kauf Einstellungen"):
|
||
- **Aktiver Zahlungsanbieter** (Select PAYONE/Stripe → `payment-active-provider`)
|
||
- **Checkout-Wartungsmodus** (Checkbox → `checkout-maintenance`)
|
||
- **Abo-Zahlungen einfrieren** (Checkbox → `abo-payments-frozen`)
|
||
- **Vorkasse ausblenden** (Checkbox → `payment-vorkasse-hidden`)
|
||
|
||
Alle Schalter sind damit ohne Tinker im Admin schaltbar (bestehender
|
||
`SettingController`-Mechanismus). `SysLog`-Protokollierung folgt mit dem Rest von S7.1.
|
||
|
||
**Tests:** Retry-Freeze, Cron-Skip (Abo bleibt Status 2, keine `user_abo_orders`),
|
||
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 — ✅ durchgeführt am 2026-07-08 abends
|
||
|
||
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`.
|
||
*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. [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. [x] **Nach S4/S5:** Deployment + 3 neue Migrationen, `abo-payments-frozen`
|
||
aufgehoben, Vorkasse wieder eingeblendet — ✅ **live seit 2026-07-10, läuft
|
||
fehlerfrei** (→ §16).
|
||
|
||
---
|
||
|
||
## 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 (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_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. |
|
||
| S4.5 | Fehler-Mapping: `card_declined`, `authentication_required`, `insufficient_funds` → bestehende Abo-Fehlerstatus (Status 3 / Retry) wie bei PAYONE-Declines; Ergebnis-DTO vereinheitlicht (`AboChargeResult`). |
|
||
| 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). |
|
||
|
||
### 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
|
||
- [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 – Zahlungsart ändern über Stripe · Status: **ERLEDIGT** (2026-07-09, E2E über CRM + Kunden-Link; nur Portal-Pfad ohne Sichttest)
|
||
|
||
### Ziel
|
||
**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::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. |
|
||
|
||
### 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.
|
||
|
||
---
|
||
|
||
## 10. Phase S6 – Fehlerfälle, 3DS-Recovery & Incidents
|
||
|
||
### Ziel
|
||
Fehlgeschlagene Stripe-Abbuchungen münden in die bestehenden Recovery-Werkzeuge; das
|
||
Payment-Dashboard kennt Stripe.
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe |
|
||
|---|---------|
|
||
| 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. **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.
|
||
- Recovery-Checkout mit Stripe-Abo Ende-zu-Ende (gemockt).
|
||
|
||
---
|
||
|
||
## 11. Phase S7 – Admin & Betrieb
|
||
|
||
### Aufgaben
|
||
|
||
| # | Aufgabe |
|
||
|---|---------|
|
||
| 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 (`/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). **ERLEDIGT ✅ (2026-07-10), siehe Umsetzungsstatus S7.4 unten.** |
|
||
| 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.4 (ERLEDIGT ✅, 2026-07-10)
|
||
|
||
`/admin/payment-dashboard` kennt jetzt beide Provider (Alt-Zeilen ohne Provider-Wert
|
||
zählen defensiv als PAYONE):
|
||
|
||
- **Provider-Status/Uptime:** Stripe in `getProviderStats()`/`getUptimeStats()` aktiviert;
|
||
`payment:check-uptime` prüft zusätzlich `https://api.stripe.com/v1/` (401 ohne Key
|
||
belegt Erreichbarkeit → kein API-Key nötig) inkl. Auto-Incident/Auto-Resolve wie PAYONE.
|
||
Incident-Kachel zeigt Stripe+PAYONE getrennt (`stripe_incidents_30d`).
|
||
- **Zahlungen** (`payments`): Anbieter-Filter (Stripe/PAYONE), Provider-Badge statt
|
||
rohem clearingtype, Umsatz-je-Provider-Kacheln (bezahlt/gesamt + Summe), Deep-Link
|
||
ins Stripe-Dashboard an Zahlung (external_reference `pi_…`, testmodus-bewusst) und
|
||
an jeder Stripe-Transaktion (txid). Helper: `ShoppingPayment::stripeDashboardUrl()`,
|
||
`PaymentTransaction::stripeDashboardUrl()`.
|
||
- **Rohe Transaktionen** (`transactions`): Anbieter-Filter + -Spalte, Karte
|
||
„Erfolgsraten je Anbieter". **Bugfix nebenbei:** `request()->string()` liefert ein
|
||
`Stringable` (immer truthy, `!== ''`) → der txaction-Filter griff IMMER mit leerem
|
||
Wert, die Liste war dauerhaft leer; jetzt `->toString()` wie in `payments()`.
|
||
- **Abos je Anbieter** (Index + GF-Ansicht): neue Kachel mit aktiven Abos je Provider
|
||
(`scopeActiveAbo`), Fehlerzähler (Status 3) und Fortschrittsbalken „Anteil Stripe" —
|
||
der Auslauf PAYONE → Stripe (§7.2) ist damit direkt ablesbar. GF-Ansicht zeigt
|
||
zusätzlich Zahlungsquote je Provider im Anbieter-Status.
|
||
- **Log-Viewer:** Kanal-Umschalter stripe/payone (Default stripe, Whitelist),
|
||
Tab heißt jetzt „Provider-Logs".
|
||
- **Tests:** `tests/Feature/PaymentDashboard/PaymentDashboardProviderTest.php` (9 Tests:
|
||
Filter, Stats je Provider, Abo-Auslauf, Deep-Links, Log-Kanäle, Uptime via Http::fake);
|
||
gesamte Dashboard-Suite (49 Tests) grün.
|
||
|
||
### 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.
|
||
- Refund: Stripe-Refund gegen gemockten Client; `charge.refunded`-Webhook schließt den Kreis; PAYONE-Zahlung nimmt PAYONE-/manuellen Pfad.
|
||
|
||
---
|
||
|
||
## 12. Phase S8 – Rollout & optionale Abo-Migration
|
||
|
||
### Rollout-Plan (empfohlen)
|
||
|
||
| Schritt | Inhalt |
|
||
|---|---|
|
||
| 1 | Staging/Testmodus: alle Flows mit Stripe-Testkarten + `stripe trigger`; Schalter-Wechselproben. |
|
||
| 2 | Produktion, Schalter bleibt `payone`: Deployment ist verhaltensneutral (Dark Launch). |
|
||
| 3 | Pilot: Schalter auf `stripe` in verkehrsarmer Zeit; Einmalzahlungen beobachten (Dashboard, `stripe.log`, Incidents). Rollback = Schalter zurück. |
|
||
| 4 | Abos auf Stripe freigeben (neue Abo-Abschlüsse). |
|
||
| 5 | Beobachtungsphase; PAYONE bleibt für Bestands-Abos und als Fallback aktiv. |
|
||
|
||
**Rollback-Garantie:** Der Schalter ist jederzeit rückstellbar; da jeder Vorgang seinen
|
||
Provider trägt, entstehen keine Mischzustände. Offene Stripe-Zahlungen laufen über
|
||
Webhooks normal aus.
|
||
|
||
### 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.
|
||
- 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 |
|
||
|---|---|
|
||
| Unit | Zahlarten-Mapping, Betrag→Cent-Rundung, Provider-Resolver, DTO-Mapping, Fehlercode-Mapping |
|
||
| Feature (Pest, SQLite) | Checkout-Verzweigung, Webhook-Handler (Signatur/Idempotenz/Paid-Pfad), Cron-Dispatch, Recovery, Refund — Stripe-Client **immer gemockt** |
|
||
| Regression | Gesamte PAYONE-Suite muss in jeder Phase unverändert grün sein (Schalter `payone` = Nullmessung) |
|
||
| Manuell (Testmodus) | 3DS-Testkarten, SEPA-`processing`, `stripe trigger`-Events, Schalterwechsel unter Last |
|
||
|
||
Hinweise: `route()` ist in Pest nicht domain-aware (bestehende Projektkonvention
|
||
beachten); kein echter Netzwerkverkehr in der Suite; `./vendor/bin/pint` nach jeder Phase.
|
||
|
||
## 14. Risiken & Mitigation
|
||
|
||
| Risiko | Mitigation |
|
||
|---|---|
|
||
| Regression im PAYONE-Bestand | PAYONE-Code nur per Adapter kapseln, nie umbauen; Schalter-Default `payone`; volle Regressionssuite pro Phase |
|
||
| Doppel-Abbuchung durch Cron-Retry | Stripe Idempotency-Keys pro `user_abo_order`; Tests dafür in S4 |
|
||
| Webhook-Verlust → Order hängt in „offen" | `stripe:sync-payment-status`-Command (S6.4); Stripe-Retry nutzt non-2xx |
|
||
| „Bezahlt" fälschlich aus Browser-Return | Paid-Flag ausschließlich im Webhook-Pfad (wie PAYONE-TSOK) |
|
||
| SEPA-Zahlungen tagelang `processing` | Eigener Anzeige-Status; Versand-/Abo-Logik erst bei `succeeded` (Klärung mit Stakeholder, ob Versand auf `processing` warten muss) |
|
||
| Schalter wird versehentlich umgelegt | Bestätigungsdialog, Admin-Rolle, SysLog, Dashboard-Anzeige des aktiven Providers |
|
||
| Stripe-Konto-Einschränkungen (Radar, Verifizierung) | Früh Live-Verifizierung anstoßen (S0/O7); Pilotphase klein halten |
|
||
| Scope-Kriechen Richtung Stripe Billing | Architektur-Entscheidung (Konzept §2) ist verbindlich dokumentiert |
|
||
|
||
## 15. Betroffene Dateien (Vorschau)
|
||
|
||
### Neu
|
||
```
|
||
config/stripe.php
|
||
app/Enums/PaymentProvider.php
|
||
app/Services/Payment/PaymentProviderManager.php
|
||
app/Services/Payment/Contracts/PaymentGatewayInterface.php (+ DTOs)
|
||
app/Services/Payment/PayoneGateway.php ← Adapter, kein Umbau
|
||
app/Services/Payment/Stripe/StripeClientWrapper.php
|
||
app/Services/Payment/Stripe/StripeCustomerService.php
|
||
app/Services/Payment/Stripe/StripePaymentService.php
|
||
app/Services/Payment/Stripe/StripeWebhookHandler.php
|
||
app/Services/Payment/Stripe/StripeTransactionRecorder.php
|
||
app/Services/Payment/Stripe/DTOs/*.php
|
||
app/Http/Controllers/Api/StripeWebhookController.php
|
||
app/Console/Commands/StripeSyncPaymentStatus.php
|
||
database/migrations/*_add_payment_provider_columns_*.php (+ Stripe-Felder)
|
||
app/Http/Controllers/Web/StripeCheckoutReturnController.php (success/cancel)
|
||
dev/2026-07-08-stripe/* (dieser Ordner: Plan, Analyse, Konzept, Runbook)
|
||
```
|
||
|
||
### Anpassung (provider-bewusst, PAYONE-Pfad unverändert)
|
||
```
|
||
app/Http/Controllers/Web/CheckoutController.php
|
||
app/Cron/UserMakeOrder.php
|
||
app/Services/AboHelper.php
|
||
app/Services/AboRetryPaymentService.php
|
||
app/Services/AboRecoveryCheckoutService.php
|
||
app/Services/AboPaymentMethodService.php
|
||
app/Services/OrderPaymentService.php
|
||
app/Http/Controllers/Admin/OrderWithdrawalController.php
|
||
app/Http/Controllers/Admin/PaymentDashboardController.php
|
||
routes/api.php, routes/domains/*.php
|
||
Abo-/Order-Views (Provider-Badge, maskiertes Zahlungsmittel)
|
||
```
|
||
|
||
## 16. Nächste Schritte (Stand 2026-07-09)
|
||
|
||
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 + Freeze aufheben**~~ ✅ **LIVE seit 2026-07-10**
|
||
(Stakeholder): S4/S5/S7.6 deployt, Abo-Freeze aufgehoben, Vorkasse wieder sichtbar —
|
||
läuft bisher fehlerfrei. **Weiter beobachten:** die ersten 04:00-Cron-Läufe
|
||
(`abo_order.log`, `stripe.log`, `stripe_webhook_events`, Payment-Dashboard) und 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**~~ ✅ umgesetzt (2026-07-10, siehe Umsetzungsstatus in
|
||
§11): Provider-Filter/-Badges, Stripe-Deep-Links, Umsatz-/Erfolgsraten je Provider,
|
||
Abos-je-Anbieter-Kachel (Auslauf-Fortschritt), Stripe-Uptime-Check, Log-Viewer für
|
||
stripe/payone. **Offen:** kurzer Sichttest im Browser.
|
||
6. Verbleibend aus S7: S7.1 (SysLog für Schalter), S7.2 (Refunds), S7.3
|
||
(Admin-Detailansichten mit Provider-Badge/Deep-Links), S7.5 (Betriebs-Runbook).
|
||
7. Nachgelagert: **S9 Rückbuchungen & Stornierungen** (§12.1).
|