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>
8.2 KiB
Ist-Analyse: Zahlungsarchitektur vor der Stripe-Einführung
Stand: 2026-07-08 · Grundlage für
ENTWICKLUNGSPLAN-STRIPE.mdVorarbeiten: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 auspayment_methods(Flagsactive,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 ruftsetPrePayment().
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) – ModelsCountry,CountryPrice,ShippingCountry. - Der Warenkorb (
packages/digital-bird/shoppingcart) arbeitet mittaxRateundtaxIncludedpro 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
- PAYONE-Tokens sind nicht portabel.
pseudocardpan/payone_useridkö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. - 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).
Pay\PayoneControllerist @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).- Multi-Tenant-Routing: Neue Web-Routen (Checkout-Return, Webhook) müssen in die
richtigen
routes/domains/*.phpbzw.routes/api.php; Webhooks laufen global unterapi.<domain>. - 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 $clientbei PAYONE). - Sofortüberweisung (
sb#PNT): Stripe hat „Sofort" abgekündigt; Ersatz wäre Klarna oder Wegfall → offene Frage an Stakeholder. - Composer-Abhängigkeit:
stripe/stripe-phpist noch nicht installiert; Ergänzung braucht Freigabe (Projektregel: keine Dependency-Änderungen ohne Zustimmung).