Stripe-Integration (S0–S3): Einmalzahlungen über Stripe Checkout + Notfall-Go-Live
Wegen PAYONE-Ausfall vorgezogen live geschaltet: alle Einmalzahlungen (Shop + Salescenter) laufen über Stripe Hosted Checkout, Abos sind bis Phase S4 eingefroren. - Provider-Abstraktion: PaymentProvider-Enum, PaymentGatewayInterface, PaymentProviderManager (Settings-Schalter, provider-treu je Bestand), PayoneGateway als unveränderter Adapter - StripePaymentService: Checkout Session (eine Gesamtposition, Metadata, success/cancel-Redirects), StripeClientWrapper (lazy, Dark Launch), StripeCustomerService, StripeTransactionRecorder (client_secret-Redaction) - Webhook /stripe/webhook: Signaturprüfung, Idempotenz über stripe_webhook_events (Crash-Retry-Reprocessing), txaction-Prioritäten, Betrags-Guard, lockForUpdate-Paid-Pfad, genau eine Bestätigungsmail - Zahlungsmethoden-Anzeige aus charge.succeeded (carddata inkl. Wallet, auftragsunabhängiges Backfill aus dem Event-Store) - Checkout: Wartungsmodus mit Admin-Bypass, Abo-Freeze-Hinweis, Stripe-Option (Online-Zahlung) statt Zahlartenwahl, Vorkasse ausblendbar - Abo-Freeze-Guards: Cron user:make_abo_order, Retry, Kartenupdate - Admin-Settings-Karte Zahlungen (Provider, Wartung, Freeze, Vorkasse) - Hotfix: 500 bei Vorkasse ohne Clearing-Daten in admin/sales/_detail und checkout-final (PAYONE-Ausfall) - Doku dev/2026-07-08-stripe/ (Plan S0–S8, Konzept, IST-Analyse) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
671022a28d
commit
6f21845607
48 changed files with 4428 additions and 831 deletions
133
dev/2026-07-08-stripe/ANALYSE-IST-ZUSTAND.md
Normal file
133
dev/2026-07-08-stripe/ANALYSE-IST-ZUSTAND.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Ist-Analyse: Zahlungsarchitektur vor der Stripe-Einführung
|
||||
|
||||
> **Stand:** 2026-07-08 · Grundlage für `ENTWICKLUNGSPLAN-STRIPE.md`
|
||||
> Vorarbeiten: `dev/2026-06-09-payments/ENTWICKLUNGSPLAN-ZAHLUNGSANBIETER.md` (Phase R abgeschlossen),
|
||||
> `dev/2026-06-25-kreditkarte-update/` (CC-Update-Feature)
|
||||
|
||||
## 1. Zahlungsfluss heute (PAYONE)
|
||||
|
||||
```
|
||||
Checkout (Web/CheckoutController) ──┐
|
||||
Abo-Cron (app/Cron/UserMakeOrder) ──┤
|
||||
Membership (PaymentHelper) ──┤
|
||||
▼
|
||||
Pay\PayoneController (@deprecated Fassade)
|
||||
│
|
||||
▼
|
||||
app/Services/Payment/Payone/PayonePaymentService
|
||||
├── PayoneRequestBuilder + PaymentMethodResolver
|
||||
├── PayoneTransactionRecorder (einzige Stelle für PaymentTransaction)
|
||||
└── App\Services\Payone (HTTP-Client → api.pay1.de)
|
||||
│
|
||||
▼
|
||||
Api\PayoneController → PayoneWebhookHandler (IPN/TSOK)
|
||||
│
|
||||
▼
|
||||
Payment::paymentStatusPaidAction()
|
||||
(Bestellung bezahlt, Abo anlegen, Mails, Incentive)
|
||||
```
|
||||
|
||||
**Wichtig:** Durch Phase R (2026-06-10) ist die PAYONE-Logik bereits in einer testbaren
|
||||
Service-Schicht unter `app/Services/Payment/Payone/` gekapselt. Das ist die ideale
|
||||
Ausgangslage, um Stripe **parallel** daneben zu stellen, statt PAYONE anzufassen.
|
||||
|
||||
## 2. Kernerkenntnis: Das System ist seine eigene Billing-Engine
|
||||
|
||||
Die wiederkehrenden Abo-Abbuchungen werden **nicht vom Zahlungsanbieter** getriggert,
|
||||
sondern vom eigenen Cron:
|
||||
|
||||
- `user:make_abo_order` (04:00 Uhr) → `app/Cron/UserMakeOrder.php`
|
||||
- Der Cron berechnet den Betrag pro Ausführung selbst (Produkte, Länderpreise, Steuern)
|
||||
und ruft dann `PayonePaymentService::chargeAbo()` mit gespeichertem Token
|
||||
(`payone_userid` + `pseudocardpan`) auf.
|
||||
- Rechnungen, Lieferscheine, Provisionsdaten entstehen im System (dompdf, DATEV-Export).
|
||||
|
||||
**Konsequenz für Stripe:** Wir brauchen von Stripe nur „belaste gespeichertes
|
||||
Zahlungsmittel mit Betrag X" (PaymentIntents, off-session) – **nicht** Stripe Billing /
|
||||
Stripe Subscriptions / Stripe Invoicing. Details und Begründung im Konzept
|
||||
(`STRIPE-KONZEPT.md`, Abschnitt 2).
|
||||
|
||||
## 3. Datenmodell (Zahlung & Abo)
|
||||
|
||||
| Tabelle / Model | Relevante Felder | Rolle |
|
||||
|---|---|---|
|
||||
| `user_abos` / `UserAbo` | `payone_userid`, `clearingtype` (`cc`/`wlt`), `wallettype` (`PPE`), `carddata` (JSON: `pseudocardpan`, `cardexpiredate`), `card_update_pending_at` | Zahlungs-Token für wiederkehrende Abbuchung |
|
||||
| `shopping_payments` / `ShoppingPayment` | `clearingtype`, `carddata`, `reference`, `is_abo`, `mode` (`test`/`live`) | Jede Zahlungstransaktion |
|
||||
| `payment_transactions` / `PaymentTransaction` | `txid`, `userid`, `txaction`, `transmitted_data` | Anbieter-Callback-Protokoll (heute PAYONE) |
|
||||
| `user_abo_orders` / `UserAboOrder` | `paid`, `status` | Ausführungs-Historie pro Abo |
|
||||
| `payment_methods` / `PaymentMethod` | `short` (`CC`,`PP`,`SEPA`,`SB`,`VOR`,`FNC`), `active`, `is_abo` | Steuert, welche Zahlarten im Checkout angeboten werden |
|
||||
| `user_abo_card_changes`, `abo_card_update_links` | – | CC-Update-Feature (2026-06-25) |
|
||||
| `order_withdrawals` / `OrderWithdrawal` | – | Widerruf/Erstattung (neu, Juli 2026) |
|
||||
|
||||
**Bekannte Lücke (bereits im Alt-Plan notiert):** Es gibt **kein `payment_provider`-Feld** –
|
||||
der Anbieter wird implizit über `clearingtype`/`wallettype` abgeleitet. Für Stripe zwingend
|
||||
zu ergänzen (Stakeholder-Entscheid O1 im Alt-Plan: `payone_userid` bleibt bestehen,
|
||||
Provider-Felder kommen **zusätzlich**).
|
||||
|
||||
## 4. Zahlarten-Auswahl im Checkout
|
||||
|
||||
- `CheckoutRepository::getPaymentsMethods()` ([CheckoutRepository.php:237](../../app/Repositories/CheckoutRepository.php))
|
||||
liest aktive Methoden aus `payment_methods` (Flags `active`, `is_abo`), zusätzlich
|
||||
User-spezifische Freischaltungen (`$user->payment_methods`).
|
||||
- `isPaymentsMethodsActive()` mappt PAYONE-Codes → Kürzel:
|
||||
`wlt#PPE`→PP, `cc`→CC, `sb#PNT`→SB, `elv`→SEPA, `vor`→VOR, `fnc#MIV`→FNC.
|
||||
- `CheckoutController::processPaymentMethod()` (Z. ~262 ff.) verzweigt je Methode
|
||||
(CC → creditcardcheck/pseudocardpan, ELV → Bankdaten-Check) und ruft
|
||||
`setPrePayment()`.
|
||||
|
||||
**Konsequenz:** Der Checkout kennt bereits ein Schalt-Modell pro Zahlart. Der
|
||||
Stripe/PAYONE-Schalter dockt hier an (Provider-Zuordnung je Zahlart bzw. global).
|
||||
|
||||
## 5. Netto/Brutto & Steuern je Land
|
||||
|
||||
- Preise pro Land in `country_prices` (`c_price`, `c_tax`, `c_currency`) –
|
||||
Models `Country`, `CountryPrice`, `ShippingCountry`.
|
||||
- Der Warenkorb (`packages/digital-bird/shoppingcart`) arbeitet mit `taxRate` und
|
||||
`taxIncluded` pro Position: Bruttopreise inkl. länderspezifischer MwSt.
|
||||
- Netto-Rückrechnung wo nötig, z. B. `AboOrderCart` (Z. ~351):
|
||||
`lineNet = lineGross / ((100 + taxRate) / 100)`.
|
||||
- Rechnungen (dompdf) und DATEV-Export (`DatevExportService`) nutzen diese Werte.
|
||||
|
||||
**Konsequenz:** Die Steuerlogik ist **anbieterunabhängig** und bleibt vollständig im
|
||||
System. Stripe erhält nur den finalen **Bruttobetrag in Cent** + Währung. Stripe Tax wird
|
||||
**nicht** benötigt (siehe Konzept, Abschnitt 5).
|
||||
|
||||
## 6. Wiederverwendbare Bausteine
|
||||
|
||||
| Baustein | Datei | Nutzen für Stripe |
|
||||
|---|---|---|
|
||||
| Service-Schicht-Muster | `app/Services/Payment/Payone/*` | 1:1-Vorlage für `app/Services/Payment/Stripe/*` |
|
||||
| Webhook-Muster (Idempotenz, Duplikate, txaction-Priorität) | `PayoneWebhookHandler` | Vorlage für `StripeWebhookHandler` |
|
||||
| Transaction-Recorder mit PCI-Redaction | `PayoneTransactionRecorder` | Vorlage für `StripeTransactionRecorder` |
|
||||
| Paid-Pfad (Bestellung → Abo → Mails) | `App\Services\Payment::paymentStatusPaidAction()` | Provider-neutral wiederverwenden |
|
||||
| Abo-Recovery-Checkout (Status 3 → Nachzahlung) | `AboRecoveryCheckoutService` | Für fehlgeschlagene Stripe-Abbuchungen (inkl. 3DS-Fall) |
|
||||
| CC-Update-Flow (Inhaber-only, Link, kein Charge) | `AboCardUpdateController`, `AboCardUpdateLink`, `AboPaymentMethodService` | Muster für „Zahlungsmittel bei Stripe hinterlegen/erneuern" via SetupIntent |
|
||||
| Widerruf/Erstattung | `OrderWithdrawalController`, `OrderWithdrawal` | Andockpunkt für Stripe-Refunds |
|
||||
| Payment-Dashboard & Incidents | `Admin/PaymentDashboardController`, `PaymentIncident` (kennt bereits mehrere Provider) | Stripe als Provider ergänzen |
|
||||
| Test-/Live-Trennung | `shopping_payments.mode`, `shopping_orders.mode` | Stripe Test-/Live-Keys analog |
|
||||
| Secrets-Muster | `config/payone.php` (Hard-Fail bei fehlendem Key) | Gleiches Muster für Stripe-Keys |
|
||||
|
||||
## 7. Randbedingungen & Stolperfallen
|
||||
|
||||
1. **PAYONE-Tokens sind nicht portabel.** `pseudocardpan`/`payone_userid` können nicht zu
|
||||
Stripe migriert werden. Bestehende Abos laufen daher auf PAYONE weiter, bis der Kunde
|
||||
sein Zahlungsmittel neu hinterlegt (Wiederverwendung des Card-Update-Link-Flows).
|
||||
Eine PCI-konforme Kartendaten-Migration zwischen Prozessoren ist theoretisch über die
|
||||
Anbieter möglich, aber aufwendig → als Option in Phase S8 dokumentiert.
|
||||
2. **PayPal-direkt-Projekt ist blockiert** (Merchant-Freigabe fehlt, siehe Alt-Plan).
|
||||
Stripe unterstützt PayPal als Zahlungsart in der EU – das könnte den blockierten
|
||||
Strang ersetzen (offene Frage im Plan).
|
||||
3. **`Pay\PayoneController` ist @deprecated Fassade** – Checkout-HTTP-Flow
|
||||
(`setPrePayment`/`ResponseData`) steckt noch dort. Die Stripe-Einführung erzwingt an
|
||||
dieser Stelle die im Alt-Plan aufgeschobene Extraktion (Provider-Verzweigung im
|
||||
Checkout).
|
||||
4. **Multi-Tenant-Routing:** Neue Web-Routen (Checkout-Return, Webhook) müssen in die
|
||||
richtigen `routes/domains/*.php` bzw. `routes/api.php`; Webhooks laufen global unter
|
||||
`api.<domain>`.
|
||||
5. **Tests:** Pest auf SQLite in-memory; `route()` ist in Tests nicht domain-aware
|
||||
(siehe Memory/Projektkonvention) – Stripe-HTTP-Aufrufe im Test immer über
|
||||
injizierbaren Client mocken (gleiches Muster wie `?Client $client` bei PAYONE).
|
||||
6. **Sofortüberweisung (`sb#PNT`):** Stripe hat „Sofort" abgekündigt; Ersatz wäre Klarna
|
||||
oder Wegfall → offene Frage an Stakeholder.
|
||||
7. **Composer-Abhängigkeit:** `stripe/stripe-php` ist noch nicht installiert; Ergänzung
|
||||
braucht Freigabe (Projektregel: keine Dependency-Änderungen ohne Zustimmung).
|
||||
639
dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md
Normal file
639
dev/2026-07-08-stripe/ENTWICKLUNGSPLAN-STRIPE.md
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
# 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
|
||||
> **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: **UMGESETZT** (2026-07-08, Browser-E2E offen)
|
||||
|
||||
### 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
|
||||
- [ ] **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] 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
|
||||
|
||||
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
|
||||
`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)"**:
|
||||
☑ 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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Phase S4 – Abos über Stripe
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.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). |
|
||||
|
||||
### 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.
|
||||
|
||||
### Akzeptanzkriterien
|
||||
- [ ] Testmodus Ende-zu-Ende: Abo-Checkout → n Tage später Cron-Abbuchung off-session erfolgreich, Rechnung systemseitig.
|
||||
- [ ] PAYONE-Abos zu 100 % unberührt (Regressionssuite + Stichprobe Staging).
|
||||
|
||||
---
|
||||
|
||||
## 9. Phase S5 – Zahlungsmittel-Update über Stripe
|
||||
|
||||
### 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.
|
||||
|
||||
### 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. |
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## 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. |
|
||||
|
||||
### 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: Umsatz/Fehlerraten je Provider; Übersicht „Abos je Provider" (Fortschritt einer späteren Migration ablesbar). |
|
||||
| S7.5 | Doku: Betriebs-Runbook (Webhook neu ausliefern, Key-Rotation, Testmodus) in diesem Ordner ablegen. |
|
||||
|
||||
### 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 (nur nach Entscheid O8)
|
||||
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
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.
|
||||
268
dev/2026-07-08-stripe/STRIPE-KONZEPT.md
Normal file
268
dev/2026-07-08-stripe/STRIPE-KONZEPT.md
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# Stripe-Konzept: Architektur & API-Mapping
|
||||
|
||||
> **Stand:** 2026-07-08 · Ergänzt `ENTWICKLUNGSPLAN-STRIPE.md` (Phasen) und
|
||||
> `ANALYSE-IST-ZUSTAND.md` (Codebase). Hier stehen die **Architektur-Entscheidungen**
|
||||
> und das Mapping der Stripe-API auf unsere Domäne.
|
||||
|
||||
## 1. Leitplanken (vom Stakeholder gesetzt)
|
||||
|
||||
1. **Stripe ersetzt PAYONE als Zahlungsabwicklung** – Rechnungen, Lieferscheine,
|
||||
Provisionen etc. werden **weiterhin im System** generiert.
|
||||
2. **PAYONE wird nicht entfernt.** Es bleibt als Option bestehen und wird sauber
|
||||
separiert. Ein **Schalter in den Einstellungen** wechselt den aktiven Anbieter.
|
||||
3. Scope: **Einmalzahlungen zuerst**, dann **Abos**.
|
||||
4. **Netto/Brutto je Land** bleibt Systemaufgabe (bestehende `country_prices`-Logik).
|
||||
5. **Zahlartenwahl und Dateneingabe finden bei Stripe statt** (Stakeholder 2026-07-08):
|
||||
Der mivita-Checkout leitet per Redirect zur **Stripe-Hosted-Checkout-Seite** weiter;
|
||||
dort wählt der Kunde die Zahlungsmethode, zahlt und springt zurück. Im eigenen
|
||||
Checkout gibt es bei aktivem Stripe **keine** Zahlarten-Formulare mehr (kein
|
||||
Kreditkarten-iFrame etc.). Der PAYONE-Checkout bleibt bei Schalter `payone` wie heute.
|
||||
6. Zahlarten-Scope: **Karte, PayPal, Apple Pay, Google Pay, Amazon Pay, Klarna** und
|
||||
weitere von Stripe bereitgestellte Methoden (Dashboard-gesteuert). Alle Zahlungen in **EUR**.
|
||||
|
||||
## 2. Grundsatzentscheidung: PaymentIntents statt Stripe Billing
|
||||
|
||||
| Kriterium | Stripe Billing (Subscriptions) | **PaymentIntents + gespeicherte Payment Methods (Empfehlung)** |
|
||||
|---|---|---|
|
||||
| Wer bestimmt Betrag & Rhythmus? | Stripe (Preise/Pläne bei Stripe gepflegt) | **Unser Cron** (`user:make_abo_order`) – wie heute bei PAYONE |
|
||||
| Rechnungen | Stripe erzeugt eigene Invoices | **System erzeugt Rechnungen** (dompdf/DATEV) – Anforderung! |
|
||||
| Abo-Inhalt ändert sich pro Ausführung (Produkte, Einmalprodukte, Länderpreise) | Schwer abbildbar (Preis-Objekte müssten synchron gehalten werden) | Trivial – Betrag wird pro Abbuchung frisch berechnet |
|
||||
| Steuerlogik | Stripe Tax nötig oder Duplikation | Bleibt zu 100 % im System |
|
||||
| Parallelität zu PAYONE | Zwei völlig verschiedene Abo-Modelle | **Gleicher Lebenszyklus wie heute**, nur anderer Charge-Kanal |
|
||||
|
||||
**Entscheidung (Empfehlung):** Abos bleiben in unserer Datenhaltung (`user_abos`,
|
||||
`user_abo_orders`), der Cron berechnet wie bisher den Betrag und ruft je nach Provider
|
||||
`chargeAbo()` bei PAYONE **oder** Stripe (off-session PaymentIntent) auf. Stripe Billing,
|
||||
Stripe Invoicing und Stripe Tax werden **nicht** verwendet.
|
||||
|
||||
> Damit ist Stripe für uns ein reiner **Charge-/Token-Provider** – exakt die Rolle, die
|
||||
> PAYONE heute hat. Das minimiert den Umbau und hält beide Anbieter symmetrisch hinter
|
||||
> einer gemeinsamen Schnittstelle.
|
||||
|
||||
## 3. Stripe-Objekte ↔ unsere Domäne
|
||||
|
||||
| Unsere Domäne | PAYONE heute | Stripe |
|
||||
|---|---|---|
|
||||
| Zahlender Kunde | `payone_userid` | `Customer` (`cus_…`) – pro `User`/`Customer` einmalig anlegen, ID speichern |
|
||||
| Gespeichertes Zahlungsmittel (Abo) | `pseudocardpan` + `cardexpiredate` in `user_abos.carddata` | `PaymentMethod` (`pm_…`), attached am Customer; ID in `user_abos.stripe_payment_method_id` |
|
||||
| Einmalzahlung Checkout | `authorization` (preauth/auth) | **Checkout Session** (`cs_…`, `mode=payment`, hosted) → erzeugt intern `PaymentIntent` (`pi_…`) |
|
||||
| Abo-Erstzahlung im Checkout | `authorization` + `recurrence=recurring` | **Checkout Session** `mode=payment` + `setup_future_usage=off_session` (Zahlungsmittel wird am Customer gespeichert) |
|
||||
| Wiederkehrende Abbuchung (Cron) | `authorization` + `userid` + `pseudocardpan` | `PaymentIntent` mit `off_session=true`, `confirm=true`, `customer` + `payment_method` (API-Call, keine Hosted Page) |
|
||||
| Zahlungsmittel aktualisieren ohne Abbuchung | `creditcardcheck` (`storecarddata=yes`) | **Checkout Session** `mode=setup` (hosted) → `SetupIntent` (`seti_…`), kein Charge, SCA-fähig |
|
||||
| Anbieter-Callback | IPN/TSOK (`txaction`) | Webhook-Events (signiert, `whsec_…`) |
|
||||
| Erstattung (Widerruf) | manuell / `refund`-Request | `Refund` (`re_…`) auf PaymentIntent/Charge |
|
||||
| Test/Live | `mode` test/live + Portal | Test-Keys vs. Live-Keys (`sk_test_…`/`sk_live_…`) |
|
||||
|
||||
### 3.1 Integrationsform: Stripe Checkout (Hosted Page) — entschieden 2026-07-08
|
||||
|
||||
**Ablauf:** mivita-Checkout (Adresse, Warenkorb, AGB) → Button „Jetzt zahlen" →
|
||||
Server erstellt eine **Checkout Session** → `303 Redirect` auf `checkout.stripe.com` →
|
||||
Kunde wählt dort die Zahlart und zahlt → Redirect zurück auf unsere `success_url`
|
||||
(bzw. `cancel_url` bei Abbruch). „Bezahlt" setzt weiterhin **nur der Webhook**
|
||||
(`checkout.session.completed` / `payment_intent.succeeded`), nie der Browser-Return.
|
||||
|
||||
Warum Hosted Checkout (statt eingebettetem Payment Element):
|
||||
|
||||
- Entspricht der Stakeholder-Vorgabe: keine Zahlungsdaten-Eingabe mehr im eigenen Checkout.
|
||||
- **Apple Pay / Google Pay funktionieren out of the box** (Domain-Verifizierung übernimmt
|
||||
Stripe auf der eigenen Domain; Wallets erscheinen automatisch nur auf fähigen Geräten).
|
||||
- **Dynamic Payment Methods:** Welche Zahlarten angezeigt werden, wird im
|
||||
**Stripe-Dashboard** konfiguriert — neue Methoden aktivieren ohne Code-Deploy.
|
||||
Der Code erzwingt nur Einschränkungen (z. B. Abo-Checkout → nur wiederverwendbare Methoden).
|
||||
- Stripe optimiert Reihenfolge/Anzeige der Methoden je Gerät, Land, Betrag.
|
||||
- PCI-Aufwand minimal (SAQ-A), kein Stripe.js-Embedding in unseren Blade-Views nötig.
|
||||
|
||||
Konsequenz für den bestehenden Checkout-Schritt „Zahlungsart wählen": Bei Schalter
|
||||
`stripe` entfällt die Detailauswahl im mivita-Checkout (bzw. reduziert sich auf
|
||||
systeminterne Methoden wie Vorkasse/Rechnung + „Online bezahlen via Stripe");
|
||||
bei Schalter `payone` bleibt der heutige Schritt unverändert.
|
||||
|
||||
### 3.2 Zahlarten (Scope lt. Stakeholder: alles, was Stripe bereitstellt)
|
||||
|
||||
| Zahlart | Stripe-Typ | Einmalzahlung | Abo-fähig (off_session) | Hinweis |
|
||||
|---|---|---|---|---|
|
||||
| Kreditkarte | `card` | ✅ | ✅ | Kernfall |
|
||||
| Apple Pay | Wallet auf `card` | ✅ | ✅ (tokenisiert als Karte) | Erscheint automatisch (Safari/iOS) |
|
||||
| Google Pay | Wallet auf `card` | ✅ | ✅ (tokenisiert als Karte) | Erscheint automatisch (Chrome/Android) |
|
||||
| PayPal | `paypal` | ✅ | ✅ (Vaulting via Stripe, EU) | Ersetzt das blockierte PayPal-direkt-Projekt (O2 = ja) |
|
||||
| Klarna | `klarna` | ✅ | ⚠️ in S0 gegen aktuelle Stripe-Doku prüfen | Ersetzt entfallene Sofortüberweisung (O3) |
|
||||
| Amazon Pay | `amazon_pay` | ✅ | ⚠️ in S0 gegen aktuelle Stripe-Doku prüfen | |
|
||||
| SEPA-Lastschrift | `sepa_debit` | ✅ | ✅ (Mandat via Stripe) | `processing` dauert Tage → eigener Status |
|
||||
| Weitere (iDEAL, Bancontact, …) | dynamisch | ✅ | teils | Dashboard-gesteuert, kein Code nötig |
|
||||
| Vorkasse (VOR) | – | systemintern | – | Bleibt außerhalb des Gateways, providerunabhängig |
|
||||
| Rechnung intern (FNC/MIV) | – | systemintern | – | Bleibt außerhalb des Gateways |
|
||||
|
||||
**Regel für Abo-Checkouts:** Die Checkout Session wird auf **wiederverwendbare**
|
||||
Methoden eingeschränkt (`card` inkl. Wallets, `paypal`, `sepa_debit`; Klarna/Amazon Pay
|
||||
nur, falls die S0-Prüfung `setup_future_usage=off_session` bestätigt). Einmalzahlungen
|
||||
bekommen das volle Dashboard-Sortiment.
|
||||
|
||||
## 4. Provider-Abstraktion & Schalter
|
||||
|
||||
### 4.1 Schichtenmodell
|
||||
|
||||
```
|
||||
UI: Checkout / Portal / CRM / Zahlungslink / Abo-Cron
|
||||
│
|
||||
▼
|
||||
PaymentProviderManager (neu)
|
||||
├── providerForNewCheckout(): PaymentProvider ← liest den Einstellungs-Schalter
|
||||
├── providerFor(UserAbo|ShoppingPayment): PaymentProvider ← liest payment_provider-Spalte
|
||||
└── gateway(PaymentProvider): PaymentGatewayInterface
|
||||
│
|
||||
├──────────────────────────────┐
|
||||
▼ ▼
|
||||
PayoneGateway (Adapter um StripeGateway (neu)
|
||||
PayonePaymentService – bestehend, app/Services/Payment/Stripe/*
|
||||
unverändert)
|
||||
```
|
||||
|
||||
```php
|
||||
enum PaymentProvider: string
|
||||
{
|
||||
case Payone = 'payone';
|
||||
case Stripe = 'stripe';
|
||||
}
|
||||
|
||||
interface PaymentGatewayInterface
|
||||
{
|
||||
public function initiateCheckoutPayment(CheckoutPaymentContext $ctx): PaymentInitiationResult;
|
||||
public function chargeAbo(UserAbo $abo, ShoppingOrder $order, ShoppingUser $user, int $amountCents, string $currency): AboChargeResult;
|
||||
public function setupPaymentMethodUpdate(UserAbo $abo): PaymentMethodUpdateSession; // CC-Update / SetupIntent
|
||||
public function refund(ShoppingPayment $payment, int $amountCents, string $reason): RefundResult;
|
||||
}
|
||||
```
|
||||
|
||||
**Wichtig:** PAYONE-Code wird dafür **nicht umgebaut** – der `PayoneGateway` ist ein
|
||||
dünner Adapter um den bestehenden `PayonePaymentService`. Ein Interface-Mismatch wird im
|
||||
Adapter gelöst, nie im Bestandscode.
|
||||
|
||||
### 4.2 Der Schalter (Einstellungen)
|
||||
|
||||
- **Neue Setting** `payment.active_provider` (`payone` | `stripe`), gepflegt im
|
||||
Admin-Bereich (CRM), Anzeige-/Änderungsprotokoll über bestehendes `SysLog`-Muster.
|
||||
- Der Schalter wirkt **nur auf neue Zahlungsvorgänge** (Checkout-Start, neue Abos,
|
||||
neue Zahlungslinks).
|
||||
- **Bestandsdaten sind schaltunabhängig:** Jede `shopping_payments`-Zeile und jedes
|
||||
`user_abos` trägt seinen `payment_provider` fest. Ein PAYONE-Abo wird auch nach dem
|
||||
Umschalten weiter über PAYONE abgebucht; Refunds gehen immer an den Provider der
|
||||
Ursprungszahlung.
|
||||
- Granularität: Start mit **globalem** Schalter; optional später pro Zahlart
|
||||
(`payment_methods.provider`-Spalte ist als Erweiterung vorgesehen) → **O4**.
|
||||
- Empfohlene Ablage: eigene kleine Tabelle/Model `PaymentSetting` oder Erweiterung des
|
||||
bestehenden Settings-Mechanismus (`Setting`/`SySetting`) → im Plan Phase S1 entscheiden,
|
||||
Konvention der Codebase folgt.
|
||||
|
||||
### 4.3 Datenmodell-Erweiterungen
|
||||
|
||||
```sql
|
||||
-- user_abos
|
||||
payment_provider VARCHAR(20) NOT NULL DEFAULT 'payone' -- Enum-Werte s. o.
|
||||
stripe_customer_id VARCHAR(255) NULL
|
||||
stripe_payment_method_id VARCHAR(255) NULL
|
||||
-- payone_userid, clearingtype, carddata bleiben unverändert (Stakeholder-Entscheid O1 Alt-Plan)
|
||||
|
||||
-- shopping_payments
|
||||
payment_provider VARCHAR(20) NOT NULL DEFAULT 'payone'
|
||||
external_reference VARCHAR(255) NULL -- Stripe PaymentIntent-ID (pi_…)
|
||||
|
||||
-- payment_transactions
|
||||
provider VARCHAR(20) NOT NULL DEFAULT 'payone' -- Webhook-Protokoll beider Anbieter
|
||||
|
||||
-- users / customers (oder eigene Tabelle stripe_customers)
|
||||
stripe_customer_id VARCHAR(255) NULL
|
||||
```
|
||||
|
||||
Backfill-Migration: alle Bestandszeilen `payment_provider = 'payone'` (Default deckt
|
||||
das ab). Keine Änderung an bestehenden PAYONE-Spalten.
|
||||
|
||||
## 5. Netto/Brutto & Währungen
|
||||
|
||||
- Betragsermittlung bleibt vollständig im System: `country_prices.c_tax` → Cart
|
||||
(`taxRate`, `taxIncluded`) → Bruttobetrag. Rechnung weist Netto/MwSt/Brutto aus wie
|
||||
heute.
|
||||
- Stripe erhält ausschließlich: `amount` = **Bruttobetrag in Cent**, `currency`,
|
||||
`description`/`metadata` (Order-Referenz). Keine Steuerdaten an Stripe, kein
|
||||
Stripe Tax.
|
||||
- Metadata-Konvention (für Support/Abgleich): `shopping_order_id`, `shopping_payment_id`,
|
||||
`user_abo_id` (falls Abo), `mode`.
|
||||
- Währung: heute faktisch EUR; `country_prices.c_currency` existiert → klären, ob
|
||||
Nicht-EUR-Länder aktiv sind → **O5**.
|
||||
|
||||
## 6. SCA / 3-D Secure
|
||||
|
||||
| Situation | Verhalten |
|
||||
|---|---|
|
||||
| Checkout (on-session) | Die Stripe-Hosted-Checkout-Seite übernimmt die 3DS-Challenge komplett. Danach Redirect auf unsere `success_url` → Status-Anzeige (UX) → „bezahlt" via Webhook. |
|
||||
| Abo-Erstzahlung | `setup_future_usage=off_session` in der Checkout Session → Bank weiß, dass Folgezahlungen kommen (MIT-Rahmen). |
|
||||
| Wiederkehrende Abbuchung (Cron, off-session) | Normalfall: keine SCA nötig (Merchant Initiated). Ausnahme `authentication_required`: Abbuchung schlägt fehl → Abo auf Status 3, Kunde bekommt Link in den **bestehenden Recovery-Checkout** (`AboRecoveryCheckoutService`) und zahlt dort on-session (Hosted Checkout) mit 3DS. |
|
||||
| Zahlungsmittel-Update | Checkout Session `mode=setup` (hosted) → SCA wird dabei gleich mit erledigt, Folgezahlungen laufen sauber. |
|
||||
|
||||
## 7. Webhooks
|
||||
|
||||
- Endpoint: `POST /api/stripe/webhook` (in `routes/api.php`, läuft global unter
|
||||
`api.<domain>` – analog PAYONE-IPN). Ohne CSRF, **mit Signaturprüfung**
|
||||
(`Stripe-Signature`-Header, `STRIPE_WEBHOOK_SECRET`).
|
||||
- Handler nach dem Muster `PayoneWebhookHandler` (Idempotenz über `event.id`,
|
||||
Duplikat-Erkennung, Persistenz **nur** über `StripeTransactionRecorder`).
|
||||
- Relevante Events (Minimal-Set):
|
||||
|
||||
| Event | Reaktion |
|
||||
|---|---|
|
||||
| `checkout.session.completed` | Session→Order-Zuordnung festziehen; bei `payment_status=paid` Paid-Pfad, bei `unpaid` (SEPA etc.) auf `payment_intent.succeeded` warten |
|
||||
| `checkout.session.expired` | Offenen Checkout-Vorgang aufräumen (Session-TTL 24 h) |
|
||||
| `payment_intent.succeeded` | Paid-Pfad (`paymentStatusPaidAction`-Äquivalent): Order bezahlt, Abo anlegen/fortschreiben |
|
||||
| `payment_intent.payment_failed` | Fehlerpfad; bei Abo → Retry-/Recovery-Logik, Incident |
|
||||
| `payment_intent.processing` | SEPA: Zahlung unterwegs → Status „in Verarbeitung" (Muster aus Doppelabo-Schutz 5.2.1 Alt-Plan) |
|
||||
| `charge.refunded` | Widerrufs-/Erstattungsstatus aktualisieren (`OrderWithdrawal`) |
|
||||
| `charge.dispute.created` | Incident im Payment-Dashboard |
|
||||
| `setup_intent.succeeded` | Zahlungsmittel-Update abschließen (`user_abos` aktualisieren) |
|
||||
|
||||
- **Redirect-Return ≠ Wahrheit:** Wie beim PAYONE-TSOK gilt der Webhook als Quelle der
|
||||
Wahrheit für „bezahlt", der Browser-Return nur für UX. SEPA wird z. B. erst Tage
|
||||
später `succeeded`.
|
||||
|
||||
## 8. Sicherheit & Compliance
|
||||
|
||||
| Thema | Umsetzung |
|
||||
|---|---|
|
||||
| PCI-DSS | **Stripe Hosted Checkout** → SAQ-A (geringstmöglicher Scope). Kartendaten berühren weder unseren Server noch unser Frontend. Secret Key nur serverseitig; ein Publishable Key wird beim Redirect-Flow nicht einmal im Frontend benötigt. |
|
||||
| Secrets | `config/stripe.php` mit `env('STRIPE_SECRET')`, `env('STRIPE_KEY')`, `env('STRIPE_WEBHOOK_SECRET')`; Hard-Fail-Muster wie `config/payone.php` (Boot-Exception bei fehlendem Secret im Live-Betrieb). Kein `env()` außerhalb von Config-Dateien. |
|
||||
| Logging | Recorder-Redaction wie PAYONE: keine vollständigen PM-Details in `transmitted_data`/Logs; Stripe-IDs (`pi_`, `pm_`, `cus_`) sind unkritisch und ausdrücklich erwünscht (Traceability). |
|
||||
| Webhook | Signaturprüfung + Idempotenz-Store; bei Verarbeitungsfehler kein 200 → Stripe wiederholt (gleiches Prinzip wie TSOK-Retry). |
|
||||
| AuthZ | Zahlungsmittel-Update nur Abo-Inhaber (Regel aus CC-Update-Projekt: „nur Inhaber gibt Karte ein" gilt unverändert für Stripe). |
|
||||
| Test/Live | Stripe-Testmodus ↔ `mode=test`; Rechnungen weiterhin nur in Produktion (bestehende Test-System-Sperre). |
|
||||
|
||||
## 9. SDK-Entscheidung
|
||||
|
||||
**Empfehlung: `stripe/stripe-php` (offizielles SDK), kein Laravel Cashier.**
|
||||
|
||||
Begründung: Cashier ist um Stripe Billing/Subscriptions herum gebaut (eigene Tabellen
|
||||
`subscriptions`, Trait am User-Model) – genau das Modell, das wir laut Abschnitt 2
|
||||
nicht verwenden. Das nackte SDK passt zur bestehenden Architektur
|
||||
(Service-Schicht + injizierbarer Client wie `?Client $client` bei PAYONE) und bleibt
|
||||
testbar (`StripeClient`-Interface-Wrapper, im Test gemockt).
|
||||
|
||||
→ Composer-Ergänzung `stripe/stripe-php`: **freigegeben** (O6, 2026-07-08).
|
||||
|
||||
## 10. Offene Fragen (Stakeholder) — beantwortet 2026-07-08
|
||||
|
||||
| ID | Frage | Antwort (Stakeholder, 2026-07-08) |
|
||||
|----|-------|-----------------------------------|
|
||||
| O1 | Welche Zahlarten zum Stripe-Start? | **Alle verfügbaren:** Karte, PayPal, Apple Pay, Google Pay, Amazon Pay, Klarna + weitere Stripe-Methoden. Anzeige über Hosted Checkout / Dashboard (s. §3.1/§3.2). Abo-Checkouts auf wiederverwendbare Methoden eingeschränkt. |
|
||||
| O2 | PayPal über Stripe? | **Ja** — implizit durch O1; ersetzt das blockierte PayPal-direkt-Projekt aus dem Alt-Plan. |
|
||||
| O3 | Ersatz für Sofortüberweisung? | **Klarna** (Teil des Zahlarten-Scopes aus O1). |
|
||||
| O4 | Schalter-Granularität | Global starten (Empfehlung angenommen, kein Widerspruch); pro Zahlart als Ausbaustufe. |
|
||||
| O5 | Nicht-EUR-Währungen? | **Nein — alle Zahlungen in EUR.** |
|
||||
| O6 | Freigabe `stripe/stripe-php`? | **Ja, freigegeben.** |
|
||||
| O7 | Stripe-Account? | **Angelegt.** Stakeholder hat Zugang und verwaltet ihn (Keys, Webhook-Konfiguration, Dashboard-Zahlarten). |
|
||||
| O8 | Migration Bestands-Abos? | **Ja, aber später** — bewusst nach hinten gestellt (Phase S8 bleibt am Ende des Plans; kein Blocker für den Rollout). |
|
||||
| O9 | Membership-/ELV-Zahlungen im Scope? | **Nach hinten gestellt** — vorerst PAYONE-only, eigener Folgeschritt nach dem Kern-Rollout. |
|
||||
|
||||
### Neue offene Punkte (aus den Entscheidungen abgeleitet)
|
||||
|
||||
| ID | Punkt | Klärung in | Antwort |
|
||||
|----|-------|-----------|---------|
|
||||
| N1 | Klarna & Amazon Pay: unterstützt Stripe dort aktuell `setup_future_usage=off_session` (Abo-Fähigkeit)? Gegen aktuelle Stripe-Doku prüfen. | Phase S0 | offen — bis zur Prüfung gelten beide als **nur Einmalzahlung** |
|
||||
| N2 | UX des Checkout-Schritts „Zahlungsart" bei Schalter `stripe`? | beantwortet 2026-07-08 | **Keine Zahlartenwahl mehr im mivita-Checkout:** Nach Checkout + Prüfung geht es direkt zu Stripe. Für Abos schränkt die Session serverseitig auf abo-fähige Methoden ein; alles andere wird dort ausgeblendet. |
|
||||
| N3 | Vorkasse (VOR) / interne Rechnung (FNC) im Stripe-Betrieb? | beantwortet 2026-07-08 | **Interne Rechnung (FNC):** bleibt als Option im System erhalten (ist ohnehin fast überall deaktiviert), wird aktuell ausgeblendet. **Vorkasse (VOR):** läuft heute über PAYONE → bekommt einen **eigenen Sichtbarkeits-Schalter in den Einstellungen**, damit sie unabhängig vom Provider-Schalter ein-/ausgeblendet werden kann. Ist Vorkasse ausgeblendet und Stripe aktiv, entfällt der Zahlarten-Schritt vollständig. |
|
||||
Loading…
Add table
Add a link
Reference in a new issue