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>
44 KiB
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-providersteht 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
ERLEDIGTgesetzt 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 ausconfig/payone.phpvor): 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 liestenv()tolerant, derStripeClientWrapper(Phase S2) schlägt beim ersten Zugriff hart fehl, wennstripe.secretfehlt. Begründung als Kommentar inconfig/stripe.phpdokumentiert. - Enum liegt unter
app/Enums/(neu, viamake: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 --dirtygrün.
Akzeptanzkriterien
- SDK installiert,
composer.lockaktualisiert, Test-Suite weiterhin grün. - Keys nur via
.env; kein Secret im Repo. (Hard-Fail bewusst in S2 verschoben, s. Abweichung.) - Alle O-Fragen beantwortet oder explizit vertagt (N1 mit Vermerk offen).
- Test-Keys + Webhook-Secret in
.enveingetragen (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()undchargeAbo()(einziger realer Aufrufer heute).initiateCheckoutPayment()(S3),setupPaymentMethodUpdate()(S5) undrefund()(S7) werden erst mit ihrem Aufrufer ergänzt — kein toter Vertrag, kein Zwang zu Dummy-Implementierungen im Adapter. Im Interface-Docblock dokumentiert. payment_providerbleibt String-Spalte ohne Enum-Cast am Model — derPaymentProviderManagernormalisiert viaPaymentProvider::tryFrom()mit PAYONE-Fallback. So können keine bestehenden Blade-/Array-Zugriffe brechen.- Schalter-Ablage: bestehender
settings-Mechanismus statt eigener Tabelle (gleiches Muster wieabo-lock-days-change,abo-card-update-link-ttl-hours). PaymentProviderManager::gateway(Stripe)wirft bewusstRuntimeException, bis das Stripe-Gateway existiert (S2/S4) — Fail-fast statt stillem Fallback auf PAYONE.- Models:
payment_provider/stripe_*/external_reference/providerin die$fillablevonUserAbo,ShoppingPayment,PaymentTransactionaufgenommen.
Tests & Verifikation (2026-07-08)
tests/Feature/PaymentProviderManagerTest.php(neu): 12 Tests, 25 Assertions — Schalter-Default/stripe/ungültiger Wert, Backfill-Defaultpayone, Spalte schlägt Schalter, Gateway-Auflösung (PayoneGateway / Stripe-Exception),AboChargeResult-Mapping (APPROVED/ERROR), Gateway-Delegation (gemockterPayonePaymentService), 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 --dirtygrün (nur Format-Fixes inShoppingPayment).
Akzeptanzkriterien
- Schalter existiert, Default
payone, Systemverhalten identisch zu vorher. - 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_idunique, Payload redaktiert als Audit,processed_at): Migration2026_07_08_150000_create_stripe_webhook_events_table.php+ ModelStripeWebhookEvent. Race-sicher: Exists-Check + Unique-Constraint-Catch (paralleler Stripe-Retry →duplicate). - HTTP-Statuscodes statt PAYONE-„immer TSOK": ungültige Signatur →
400(kein Retry sinnvoll); fehlendesSTRIPE_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) oderexternal_reference(PaymentIntent-/Session-ID); ohne Treffer wird das Event trotzdem gespeichert und bestätigt (Business-Verdrahtung folgt in S3/S4 andispatchEvent()). - 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 (providerstripe,client_secretredaktiert), Auflösung viaexternal_referenceprocessed_at, rekursive Redaction, Wrapper-Hard-Fail ohne Secret, Lazy-Client, CustomerService (vorhandene ID ohne API-Call; Create persistiert ID — Stripe-Client als Partial-Mock aufrequest, 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:listverifiziert:POST api.mivita.test/stripe/webhook → api.stripe_webhook. - Regression:
PaymentProviderManagerTest+StripeFoundationTest+PayoneWebhookHandlerTest+PayoneTransactionRecorderTest: 35 passed, 131 Assertions.pint --dirtygrün.
Akzeptanzkriterien
PaymentTransaction-Schreibzugriffe für Stripe existieren nur im Recorder.- Webhook idempotent, signaturgeprüft, protokolliert (per Test nachgewiesen).
- Ende-zu-Ende gegen das laufende System verifiziert (2026-07-08): signierter
payment_intent.succeeded-Request anapi.mivita.test/stripe/webhook(echte Signaturprüfung mit demwhsecaus.env) →200 processed, Event-Zeile mitprocessed_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). - Echte Stripe-Events über
stripe listenverifiziert (2026-07-08): Testmodus- PaymentIntent (API,pm_card_visa) → Listener → Endpoint.payment_intent.succeededsignaturgeprüft verarbeitet (processed_atgesetzt); 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.testfehlte in der/etc/hostsdes Hosts, 3.whsecmuss 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:
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
whsecpasst nicht zu unserer Signaturprüfung. Für mivita daher einen eigenen Listener parallel starten — einfachster Weg ohne Re-Login:stripe listen --api-key sk_test_…(mivita) \ --forward-to https://api.mivita.test/stripe/webhook --skip-verifyDas dabei ausgegebene
whsec_…in die mivita-.env, dannphp artisan config:clear. Testen:stripe trigger payment_intent.succeeded --api-key sk_test_…(mivita). (Stand 2026-07-08: das zuerst eingetragenewhsecstammte vom pressekonto-Listener und muss durch das mivita-Listener-Secret ersetzt werden.)
Produktion (später, vor Rollout-Schritt 2):
- Dashboard → Developers → Webhooks → Add endpoint.
- Endpoint-URL:
https://api.mivita.care/stripe/webhook. - 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. - Das Endpoint-Signing-Secret (
whsec_…, ein anderes als das CLI-Secret!) alsSTRIPE_WEBHOOK_SECRETin 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.expiredrä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).
- 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
- Deployment des aktuellen Stands (Branch
payment/phase-r-payone-refactoring). php artisan migrate(3 neue Migrationen: Provider-Spalten,stripe_webhook_events).- Prod-
.env:STRIPE_KEY/STRIPE_SECRET= Live-Keys (pk_live_…/sk_live_…),STRIPE_MODE=live. - Stripe-Dashboard (Live-Modus): Zahlarten aktivieren; Webhook-Endpoint
https://api.mivita.care/stripe/webhookmit der Event-Liste aus §6 anlegen; dessen Signing-Secret alsSTRIPE_WEBHOOK_SECRETin die Prod-.env. - Caches:
php artisan config:clear && php artisan view:clear(bzw. Deploy-üblichconfig:cache). - 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.
- 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.
- Checkout öffnen: Wartungsmodus abhaken — Einmalzahlungen laufen für alle über Stripe.
- Kontrolle:
storage/logs/stripe.log,stripe_webhook_events, Payment-Dashboard. - Nach S4:
abo-payments-frozenzurü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 →
UserAbomit Stripe-Feldern; PAYONE-Felder leer. - Cron-Abbuchung Stripe-Abo: Erfolg (Order paid,
user_abo_ordersfortgeschrieben), 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 |
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
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).- Phase S0 starten: Test-Keys + Webhook im (vorhandenen) Stripe-Account einrichten,
stripe/stripe-phpinstallieren,config/stripe.php, N1-Prüfung (Klarna/Amazon Pay Abo-Fähigkeit). - Danach S1 → S2 → S3; nach S3 erster produktiver Meilenstein (Einmalzahlung) möglich.