Lecteur Markdown
PAYMENT_DOCUMENTATION_DE
Plugin: Payment #
Zahlungs-Rückkehrseite (Erfolg, Abbruch, Wartend). Empfängt den Benutzer nach dem Checkout über Stripe / PayPal / Überweisung / Scheck, prüft den Status, zeigt die Bestätigung an und versendet die Zusammenfassungs-E-Mail.
---
Rolle #
Das `payment`-Plugin initiiert keine Zahlung — es empfängt die Rückläufer:
| Provider | Rückkehr-URL | Aktion |
|----------|--------------|--------|
| Stripe | `?obj=payment.php&order=NUM&session_id=cs_...` | Synchrone Sitzungsprüfung, markiert die Bestellung als `paid`, wenn Stripe bestätigt |
| PayPal | `?obj=payment.php&order=NUM&token=...` | Zahlungsabbuchung über `Payment::handleCallback('paypal', …)` |
| Überweisung | `?obj=payment.php&order=NUM` | Zeigt IBAN/BIC + Bestellreferenz |
| Scheck | `?obj=payment.php&order=NUM` | Zeigt Postanschrift + Empfänger |
| Abbruch | `?obj=payment.php&action=cancel&order=NUM` | Stellt den Warenkorb wieder her, leitet zu `products_checkout.php` weiter |
Die Zahlungsinitiierung wird vorgelagert von `products_checkout` (klassischer Warenkorb), `abo_checkout` (erstes Abonnement) und `reabo_checkout` (Wieder-Abonnement) übernommen.
---
Netto / Brutto Architektur #
| Schicht | Format |
|---------|--------|
| `products.price`, `products.subscription_price` (DB) | Netto |
| `order_items.unit_price_ht`, `unit_price_recurring_ht` (DB) | Netto |
| Benutzeranzeige (Warenkorb, Produktseite, Zahlung) | Brutto |
| Stripe/PayPal API `unit_amount` (Cent) | Brutto (berechnet über `vat_rate`) |
| Admin-Editor-Eingabe | Brutto über Toggle (aber als Netto gespeichert) |
| B2B VIES nullbesteuert | beim Checkout `vat_rate=0` übergeben |
Alle an die Provider gesendeten Beträge werden über `bcmul(number_format(Netto × (1 + vat/100), 2, '.', ''), '100', 0)` berechnet, um Float-Rundungen zu vermeiden.
---
Stripe-Flow — synchrone Prüfung bei der Rückkehr #
Stripe leitet zur `success_url` weiter, bevor der Webhook die Verarbeitung immer abgeschlossen hat. Ohne synchrone Prüfung sah der Benutzer „Zahlung wird bearbeitet", obwohl alles in Ordnung war.
Stripe Checkout → success_url (payment.php?session_id=...)
↓
payment.php : Payment::verifySession('stripe', $sessionId, $orderId)
↓
PaymentStripe::verifySession()
GET /v1/checkout/sessions/{id}
Prüft: status='complete' UND payment_status IN ('paid','no_payment_required')
↓
Wenn OK → Payment::updateStatus($orderId, 'paid', $paymentIntent, 'Stripe session verified on return')
↓
Bestätigung wird angezeigt
Der Webhook bleibt die Backup-Quelle der Wahrheit (idempotent dank `Payment::updateStatus`). Wenn die synchrone Prüfung fehlschlägt (Netzwerk, langsames Stripe), übernimmt der Webhook und der Benutzer sieht „in Bearbeitung" und kann dann aktualisieren.
---
Anzeige des Gesamtbetrags mit Abonnement #
Bei gemischten Bestellungen (Einmal-Produkte + Abonnement) enthält `orders.total_ttc` nur den Einmal-Teil. Die erste Abo-Rate wird von Stripe über einen separaten `line_item` abgerechnet.
Die Anzeige berechnet daher:
$paidTodayTtc = (float)$order['total_ttc'] + $recurringTtc;
mit `$recurringTtc = Σ (unit_price_recurring_ht × (1 + vat_rate/100) × quantity)` über `order_items`, bei denen `is_subscription = 1`.
Drei angezeigte Zeilen:
- Heute gezahlt: `$paidTodayTtc`
- Davon Produkte: `$order['total_ttc']`
- Davon erste Abo-Zahlung: `$recurringTtc`
- Dann: `$recurringTtc / Monat` (wiederkehrend)
---
Bestätigungs-E-Mail #
Wird nur einmal pro Bestellung versendet (Flag `orders.email_sent`). Enthält:
- Zeilen-Zusammenfassung (Name, Menge, Brutto-Zeile)
- Netto-Zwischensumme + MwSt. + Brutto-Gesamtbetrag
- Rechnungs-/Lieferadressen
- Überweisungs-/Scheckhinweise falls zutreffend
- Stripe/PayPal-Transaktionsreferenz falls zutreffend
Versand über natives PHP `mail()`, Absender `$cfg[34]['headoffice_name'] <$cfg[11]['commercial']>`.
---
Abbruch #
Wenn `?action=cancel` UND `payment_status='unpaid'`:
1. `order_items` der Bestellung abrufen
2. Aktuellen Warenkorb leeren (`Panier::vider()`)
3. Artikel wieder einfügen (`Panier::ajouterProduit(productId, quantity)`)
4. Bestellung als `failed` markieren über `Payment::updateStatus(..., 'failed', ..., 'Payment cancelled by user')`
5. Weiterleitung zu `products_checkout.php`
Gleiche Logik, wenn der Benutzer mit einer bereits `failed`-Bestellung zu `payment.php` zurückkehrt (Warenkorb-Wiederherstellung).
---
Webhook — `handlers/payment.mod.php` #
Endpoint: `?obj=payment.mod.php&method=stripe` (oder `paypal`)
POST raw payload
↓
$rawPayload = file_get_contents('php://input')
$method = $_GET['method']
↓
Payment::handleCallback($method, ['_raw'=>…, '_headers'=>…, 'event'=>…])
↓
Provider prüft Signatur + verarbeitet das Event
↓
HTTP 200 {status:ok} | 400 {status:error}
Stripe und PayPal müssen in ihren jeweiligen Dashboards mit diesen URLs konfiguriert werden:
- `https://shop.example.com/index.php?obj=payment.mod.php&method=stripe`
- `https://shop.example.com/index.php?obj=payment.mod.php&method=paypal`
---
Konfiguration `cog.php` #
| Index | Rolle |
|-------|-------|
| `$cfg[34]` | Firmendaten (Name, Adresse, SIRET) — in der E-Mail verwendet |
| `$cfg[35]` | IBAN / BIC für Überweisungen |
| `$cfg[36]` | Stripe: `public_key`, `secret_key`, `webhook_secret`, `mode` |
| `$cfg[37]` | PayPal: `client_id`, `client_secret`, `webhook_id`, `mode` |
| `$cfg[38]` | Scheck: `order_to` (Scheck-Empfänger) |
| `$cfg[11]['commercial']` | Absender-E-Mail für Bestätigungen |
---
Abhängigkeiten #
- `Beamreactor\Database\SQL`
- `Beamreactor\Payment\Payment` — öffentliche Fassade (`processSubscription`, `verifySession`, `handleCallback`, `updateStatus`, `getInstructions`)
- `Beamreactor\Payment\PaymentStripe`, `PaymentPaypal`, `PaymentVirement`, `PaymentCheque` — konkrete Handler, autoloaded durch `Payment::loadHandler()` (expliziter include, nicht über Namespace)
- `Beamreactor\Shop\Panier` — Warenkorb-Wiederherstellung bei Abbruch/Fehler
- `Beamreactor\Sanitizer\Parser` — Bereinigung der GET-Parameter
- PHP-Funktion `mail()` zum Versenden der E-Mail
⚠️ Niemals `PaymentStripe` direkt mit `new \Beamreactor\Payment\PaymentStripe()` instanziieren — die Klasse wird nicht über den Namespace autoloaded. Immer über die statischen Methoden von `Payment::` gehen.
---
SQL-Tabellen #
- `orders`: Hauptbestellung (`payment_status`, `total_ttc`, `payment_method`, `email_sent`, …)
- `order_items`: Bestellzeilen, einschließlich Abo-Spalten (`is_subscription`, `unit_price_recurring_ht`, `subscription_billing_anchor`, `subscription_interval`)
- `payment_subscriptions`: aktive Stripe/PayPal-Abonnements (durch Webhooks gespeist)
---
Installation #
Das Verzeichnis `payment/` in `/plugins/` ablegen. Keine spezifische Migration. Die Tabellen `orders` / `order_items` werden vom Haupt-Shop-Modul verwaltet.