# Flow: Payment

**Actors**: Patient, external payment gateway (Stripe/Paymob/PayPal, via existing config)
**Phase**: 2 (`05-roadmap.md`)
**Reuses**: `App\Services\Payment\PaymentGatewayManager` + gateway adapters, `App\Models\Payment\PaymentGateway`
**New**: `clinic_payments` table/model (see `models/Payment.md` for why `PaymentTransaction` isn't reused)

## Steps

1. Patient reaches the payment step of `Site\Clinic\BookingController::store()` (booking already
   created with `status='pending'`).

2. `Site\Clinic\BookingPaymentController::initiate(Booking $booking)`:
   - Creates a `Payment` row (`clinic_payments`), `status='pending'`, `amount = $booking->price`,
     `gateway_code` = the patient's chosen/only-configured gateway.
   - Resolves the adapter: `PaymentGatewayManager::gateway($code)` → `->adapter($code)`.
   - Calls `$adapter->initiate($booking, $payment, $gateway)` — same call shape as the existing
     `Site\CartController::placeOrder()` does for `SalesOrder` (`app/Http/Controllers/Site/CartController.php:150`),
     just passing a `Booking`+`Payment` pair instead of an `Order`+`PaymentTransaction` pair.
   - Redirects to the gateway's hosted checkout page (`redirect()->away($result->redirectUrl)`).

3. **Gateway return** (patient comes back from the hosted page):
   `Site\Clinic\BookingPaymentController::return(Payment $payment)` — mirrors
   `Site\PaymentController::return()` (`app/Http/Controllers/Site/PaymentController.php:25`):
   calls `$adapter->handleReturn(...)`, then `$payment->markPaid()` or `->markFailed()`.
   `markPaid()` also flips `Booking.status` to `'confirmed'` (see `models/Payment.md`).
   Redirects to `Site\Clinic\MyBookingController::show($booking)` instead of `orders.show`.

4. **Gateway webhook** (async, gateway-initiated):
   `Site\Clinic\BookingPaymentController::webhook(Request $request, string $gatewayCode, Payment $payment)`
   — mirrors `Site\PaymentController::webhook()` (`app/Http/Controllers/Site/PaymentController.php:44`):
   validates the gateway code matches, calls `$adapter->handleWebhook(...)`, stores
   `webhook_payload`, marks paid/failed. This is the authoritative confirmation path (the
   `return()` step is a UX convenience, not the source of truth) — same as the existing e-commerce
   checkout.

5. On `markFailed()`, `Booking` stays `'pending'` — `Site\Clinic\BookingPaymentController::initiate()`
   can be called again for a retry (a booking can have multiple `Payment` attempts if the first
   fails; only the successful one matters — consider whether `clinic_payments.booking_id` should
   stay `unique()` or allow multiple rows per booking for retry history. **Open decision**: if
   retries are common, drop the `unique()` constraint on `booking_id` and instead key "the current
   payment" as the latest row — flag this for confirmation once real payment-failure rates are
   observed; `unique()` is the simpler v1 default and is what `models/Payment.md` currently specifies).

## New routes needed (`routes/web.php`, inside the existing localized group)

```
GET  /clinic/payments/{payment}/hosted    → BookingPaymentController::hosted
GET  /clinic/payments/{payment}/return    → BookingPaymentController::return
POST /clinic/payments/webhook/{gatewayCode}/{payment} → BookingPaymentController::webhook
```

Named and structured the same way as the existing `payments.hosted` / `payments.return` /
`payments.webhook` routes (`routes/web.php:32-34`), just under a `clinic.` route-name prefix and
keyed by `Payment` instead of `PaymentTransaction`.
