Lecteur Markdown
PAYMENT_DOCUMENTATION_EN
Plugin: Payment #
Payment return page (success, cancellation, pending). Receives the user after Stripe / PayPal / bank transfer / cheque checkout, verifies the status, displays the confirmation and sends the recap email.
---
Role #
The `payment` plugin does not initiate any payment — it receives the returns:
| Provider | Return URL | Action |
|----------|------------|--------|
| Stripe | `?obj=payment.php&order=NUM&session_id=cs_...` | Synchronous session verification, marks the order `paid` if Stripe confirms |
| PayPal | `?obj=payment.php&order=NUM&token=...` | Payment capture via `Payment::handleCallback('paypal', …)` |
| Transfer | `?obj=payment.php&order=NUM` | Displays IBAN/BIC + order reference |
| Cheque | `?obj=payment.php&order=NUM` | Displays the postal address + payee |
| Cancel | `?obj=payment.php&action=cancel&order=NUM` | Restores the cart, redirects to `products_checkout.php` |
Payment initiation is handled upstream by `products_checkout` (regular cart), `abo_checkout` (first subscription) and `reabo_checkout` (re-subscription).
---
HT / TTC Architecture #
| Layer | Format |
|-------|--------|
| `products.price`, `products.subscription_price` (DB) | HT (excl. VAT) |
| `order_items.unit_price_ht`, `unit_price_recurring_ht` (DB) | HT |
| User display (cart, product page, payment) | TTC (incl. VAT) |
| Stripe/PayPal API `unit_amount` (cents) | TTC (computed via `vat_rate`) |
| Editor admin input | TTC via toggle (but stored HT) |
| B2B VIES zero-rated | pass `vat_rate=0` at checkout time |
All amounts sent to providers are computed via `bcmul(number_format(HT × (1 + vat/100), 2, '.', ''), '100', 0)` to avoid float rounding.
---
Stripe flow — synchronous verification on return #
Stripe redirects to `success_url` before the webhook has always finished processing. Without synchronous verification, the user would see "payment in progress" while everything was actually OK.
Stripe Checkout → success_url (payment.php?session_id=...)
↓
payment.php : Payment::verifySession('stripe', $sessionId, $orderId)
↓
PaymentStripe::verifySession()
GET /v1/checkout/sessions/{id}
Checks : status='complete' AND payment_status IN ('paid','no_payment_required')
↓
If OK → Payment::updateStatus($orderId, 'paid', $paymentIntent, 'Stripe session verified on return')
↓
Confirmation displayed
The webhook remains the fallback source of truth (idempotent thanks to `Payment::updateStatus`). If the synchronous verification fails (network, slow Stripe), the webhook will take over and the user will see "in progress" then can refresh.
---
Total display with subscription #
For mixed orders (one-time products + subscription), `orders.total_ttc` only contains the one-time part. The first subscription instalment is billed by Stripe via a separate `line_item`.
The display therefore computes:
$paidTodayTtc = (float)$order['total_ttc'] + $recurringTtc;
with `$recurringTtc = Σ (unit_price_recurring_ht × (1 + vat_rate/100) × quantity)` over `order_items` where `is_subscription = 1`.
Three rows displayed:
- Total paid today: `$paidTodayTtc`
- Of which products: `$order['total_ttc']`
- Of which first subscription payment: `$recurringTtc`
- Then: `$recurringTtc / month` (recurring)
---
Confirmation email #
Sent only once per order (flag `orders.email_sent`). Contains:
- Line recap (name, qty, line TTC)
- Subtotal HT + VAT + total TTC
- Billing/shipping addresses
- Transfer/cheque instructions if applicable
- Stripe/PayPal transaction reference if applicable
Sent via PHP native `mail()`, sender `$cfg[34]['headoffice_name'] <$cfg[11]['commercial']>`.
---
Cancellation #
If `?action=cancel` AND `payment_status='unpaid'`:
1. Fetch the order's `order_items`
2. Empty the current cart (`Panier::vider()`)
3. Re-inject the items (`Panier::ajouterProduit(productId, quantity)`)
4. Mark the order `failed` via `Payment::updateStatus(..., 'failed', ..., 'Payment cancelled by user')`
5. Redirect to `products_checkout.php`
Same logic if the user returns to `payment.php` with an order already `failed` (cart recovery).
---
Webhook — `handlers/payment.mod.php` #
Endpoint: `?obj=payment.mod.php&method=stripe` (or `paypal`)
POST raw payload
↓
$rawPayload = file_get_contents('php://input')
$method = $_GET['method']
↓
Payment::handleCallback($method, ['_raw'=>…, '_headers'=>…, 'event'=>…])
↓
Provider verifies signature + handles the event
↓
HTTP 200 {status:ok} | 400 {status:error}
Stripe and PayPal must be configured in their respective dashboards with these URLs:
- `https://shop.example.com/index.php?obj=payment.mod.php&method=stripe`
- `https://shop.example.com/index.php?obj=payment.mod.php&method=paypal`
---
`cog.php` configuration #
| Index | Role |
|-------|------|
| `$cfg[34]` | Company details (name, address, SIRET) — used in the email |
| `$cfg[35]` | IBAN / BIC for transfers |
| `$cfg[36]` | Stripe: `public_key`, `secret_key`, `webhook_secret`, `mode` |
| `$cfg[37]` | PayPal: `client_id`, `client_secret`, `webhook_id`, `mode` |
| `$cfg[38]` | Cheque: `order_to` (cheque payee) |
| `$cfg[11]['commercial']` | Sender email for confirmations |
---
Dependencies #
- `Beamreactor\Database\SQL`
- `Beamreactor\Payment\Payment` — public façade (`processSubscription`, `verifySession`, `handleCallback`, `updateStatus`, `getInstructions`)
- `Beamreactor\Payment\PaymentStripe`, `PaymentPaypal`, `PaymentVirement`, `PaymentCheque` — concrete handlers, autoloaded by `Payment::loadHandler()` (explicit include, not by namespace)
- `Beamreactor\Shop\Panier` — cart restoration on cancel/failure
- `Beamreactor\Sanitizer\Parser` — sanitize GET parameters
- PHP `mail()` function for sending the email
⚠️ Never instantiate `PaymentStripe` directly via `new \Beamreactor\Payment\PaymentStripe()` — the class is not autoloaded by namespace. Always go through the static methods of `Payment::`.
---
SQL tables #
- `orders`: main order (`payment_status`, `total_ttc`, `payment_method`, `email_sent`, …)
- `order_items`: order lines, including subscription columns (`is_subscription`, `unit_price_recurring_ht`, `subscription_billing_anchor`, `subscription_interval`)
- `payment_subscriptions`: active Stripe/PayPal subscriptions (fed by webhooks)
---
Installation #
Drop the `payment/` directory into `/plugins/`. No specific migration. The `orders` / `order_items` tables are managed by the main shop module.