# Solo Booking — Payment Methods (Cash / Deposit / Card) — Backend Note → Mobile Team

**From:** backend · **Date:** 2026-08-16 · **Status: ✅ ALL THREE MODES LIVE** on `tailwind-poc`
(built across NVG-BEA-001 + the 2026-08-11 Issue-C delivery; this note is the missing
single-place contract, freshly re-verified end-to-end today).
**Nothing new to integrate against — this consolidates what is already deployed.**
Companion notes: `GROUP_BOOKING_DELIVERY_TO_MOBILE.md` (same vocabulary per child),
`PROMOTIONS_BOOKING_DELIVERY_TO_MOBILE.md` (discounts feed these amounts).

---

## 0. The model (30 seconds)

A solo booking is paid in exactly ONE of three modes — the customer picks at checkout:

| `payment_mode` | Customer pays now | Pays at the salon | Booking confirms |
|---|---|---|---|
| `online` | full total via Paymob | nothing | after `/booking/pay` |
| `deposit` | `deposit_percentage`% of the total via Paymob | the remaining `balance_due` | after `/booking/pay` |
| `on_visit` | nothing | full total | **immediately** at `/booking/book` |

- **The SALON controls which modes exist** (portal Settings → Payments). A salon with no
  settings row is **online-only**. Never hardcode the three options — render what
  `payment-options` returns.
- **All amounts are server-computed on the DISCOUNTED total** (promotions/invitations are
  already inside `total_amount`). Never compute splits client-side.
- Package-redemption bookings (`POST /subscription-package/redeem`) are pre-paid by
  definition — they come back `payment_mode: "package"`, all cash fields 0, and never show
  the payment chooser. (Mixing package + cash inside ONE solo booking is not supported —
  that exists only as separate children inside a group party.)

---

## 1. The flow, screen by screen

### Screen A — Checkout → "When do you want to pay?" (mockup semantics)

Radio cards, exactly these three (filtered by the salon's enabled modes), single-select:

1. **Pay online** — subtitle "Pay ⃁{total} now"
2. **Deposit {pct}%** — subtitle "Pay ⃁{deposit_amount} now · ⃁{balance_due} on visit"
3. **Pay on visit** — subtitle "Nothing now · ⃁{total} on visit"

The CTA text follows the selection: "Pay ⃁{amount} now" for online/deposit,
"Confirm booking" for on-visit.

**Data source:** after `POST /booking/booking-services` creates the draft, call
`POST /booking/payment-options` `{booking_id}` →

```json
{ "booking_id": 2423, "modes": ["on_visit", "deposit", "online"],
  "deposit_percentage": 30, "deposit_amount": 27.00, "balance_due": 63.00,
  "total_amount": 90.00, "currency": "SAR" }
```
(`deposit_*` are null when the salon disables deposits. Live-verified today: 90 total →
27/63 at 30%.)

### Screen B — per selection

- **on_visit:** ONE call — `POST /booking/book` with `payment_mode=on_visit` (+ the usual
  `booking_id, agent_id, schedule_date, from_hour, to_hour`). The response comes back
  **status 2 (Scheduled)** with `amount_collected: 0`, `balance_due: total`,
  `deposit_amount: 0`. No Paymob, no `/pay`. Go straight to the confirmation screen
  ("Paid now ⃁0.00 · Due on visit ⃁{total}").
- **online / deposit:** `POST /booking/book` with `payment_mode=online|deposit` stamps the
  intent — the booking **stays status 1 (pending payment)**. Then run the Paymob SDK for
  the amount (`total_amount` for online, `deposit_amount` for deposit — read them, don't
  compute), then settle with `POST /booking/pay`
  `{booking_id, invoice_id, integration_order_id, payment_mode}`. On "Paid" the server
  flips to **Scheduled** and stamps the breakdown atomically (idempotent per transaction —
  webhook double-settlement is impossible).

### Screen C — Confirmation

Read the booking response, never recompute:

| Mode | "Paid now" | "Due on visit" |
|---|---|---|
| online | `amount_collected` (= total) | `0.00` |
| deposit | `amount_collected` (= deposit_amount) | `balance_due` |
| on_visit | `0.00` | `balance_due` (= total) |

### Screen D — My bookings / booking detail

Every single-booking response carries the full breakdown (same vocabulary as group
children): `payment_mode`, `deposit_amount`, `amount_collected`, `balance_due`,
`refund_value`, `payment_message`. Render "⃁{balance_due} due at the salon" badges from
these. The remaining balance (deposit/on-visit) is collected **by the salon at completion**
— there is no second in-app payment step.

### Cancellations

Cancel responses carry `refund_value` + `payment_message` — display them as-is (the
server applies the salon's cancellation policy to whatever was actually collected).

---

## 2. Errors to handle

| Case | Response |
|---|---|
| Mode disabled by the salon | **422** `This payment method is not available for this shop.` (live-verified today) — re-fetch `payment-options` and re-render |
| Slot taken between quote and book | **409** "This time slot is no longer available…" |
| Promo died on the final slot | **422** with `promo_reason`/`promo_message` — see the promotions note §2 |
| Paymob verification failed at `/pay` | **404** with the gateway status string — keep the booking pending, let the customer retry |

---

## 3. QA checklist (what we verified server-side today)

- `payment-options` returns the salon's modes + 30% split math (27/63 of 90). ✓
- `book payment_mode=on_visit` → Scheduled immediately, `balance_due = total`. ✓
- `book payment_mode=deposit` → intent stamped, stays pending until `/pay`. ✓
- Disabled mode → 422. ✓
- `/pay` settlement math (deposit vs full) re-read in code — unchanged since the Issue-C
  delivery; Paymob settlement itself needs your sandbox to E2E.

Nothing changed in any shape today — if your build already implements Issue C §C.2, you
are done; this note is the consolidated reference to build the payment screens against.
