mivita/dev/2026-07-08-stripe/STRIPE-KONZEPT.md
Kevin e1da90ff91 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>
2026-07-08 15:56:41 +00:00

18 KiB
Raw Blame History

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)
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

-- 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.