# 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.` – 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. |