mivita/dev/2026-07-08-stripe/ANALYSE-IST-ZUSTAND.md
Kevin 6f21845607 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

8.2 KiB
Raw Permalink Blame History

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