# booking-requests — the money-free pre-payment request

> Client seam `client/src/services/bookingRequests/` · `USE_BOOKING_REQUESTS_MOCK = false` (**real**) · 7 server ops
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).

Stage one of the booking flow: the customer asks, the nurse answers, and **no money exists yet**. A
`booking_requests` row becomes a `bookings` row only on payment capture — see
[bookings.md](bookings.md) and [payment.md](payment.md).

> `Features/Booking` (singular, this domain) and `Features/Bookings` (plural, the post-payment engine) are
> **different server areas, not a rename.**

## Endpoints

| Method | Path | Caller | Verdict |
| --- | --- | --- | --- |
| POST | `/api/v1/booking_requests/create` | customer | wired |
| GET | `/api/v1/booking_requests/list` | both | wired · paginated · role-scoped |
| GET | `/api/v1/booking_requests/get/{id}` | both | wired |
| POST | `/api/v1/booking_requests/accept/{id}` | nurse | wired |
| POST | `/api/v1/booking_requests/reject/{id}` | nurse | wired |
| POST | `/api/v1/booking_requests/cancel/{id}` | customer | wired |
| GET | `/api/v1/booking_requests/checkout_summary/{id}` | customer | wired — but read by the **[payment](payment.md)** domain, not this one |

All `[Authorize]`. No phantoms.

`checkout_summary` is the C6 money read (gross / commission / VAT breakdown, REQ-016 delivered). It lives
on this controller because the request is what gets paid for, and is documented in
[payment.md](payment.md) where it is consumed.

## The lifecycle

```
pending_nurse_response ──accept──▸ accepted_awaiting_payment ──capture──▸ converted
         │                                    │
         ├──reject──▸ rejected_by_nurse       └──window lapses──▸ payment_deadline_expired
         ├──deadline──▸ expired_no_response
         └──customer──▸ cancelled_by_customer
```

**Forward-only.** A backward or sideways transition is a clean `409`, never a 500. Two deadlines are
server-owned and config-driven (`booking_request_response_deadline_minutes`,
`payment_window_minutes` in [admin.md](admin.md)):

- the nurse's response window → `expired_no_response`
- the customer's payment window after acceptance → `payment_deadline_expired`

`POST /api/v1/admin_booking_requests/expire` is the ops one-shot for both; the in-process scheduler runs
it unattended. See [admin.md](admin.md).

## Shape rules the JSON does not express

- **Stage-one disclosure is deliberately partial.** Before payment the nurse sees only unencrypted
  `customerNotes` and a **city/district-coarse masked address**. Encrypted care instructions and the full
  address are unreadable until the booking is confirmed. This is a hard server rule, not a UI choice.
- **The countdown is server-frozen.** `CountdownTimer` renders a deadline the server sent; the client
  never computes an expiry from a local clock.
- **`variantPrice` is on `BookingRequestDto`** (REQ-013, delivered) so the customer sees the price they are
  committing to without a second variant fetch. It is a **digit string**.
- **The inbox list item carries `variantLabel` + `patientAge`** (REQ-014, delivered).

### The two list shapes, exactly

Confirmed field-by-field against the live swagger, because three in-repo comments disagree about this:

| | `BookingRequestListItemDto` (list) | `BookingRequestDto` (detail) |
| --- | --- | --- |
| `variantLabel` | **yes** | yes |
| `patientAge` | **yes** | — (`patientName` instead) |
| `variantPrice` · `variantPriceUnit` | **no** | yes |
| address / notes | `customerNotes` only | full masked address block |
| `nurseRejectionReason` | — | yes, **free text** |

Two consequences:

- **`client/src/services/bookingRequests/types.ts` is behind the wire.** It marks `variantLabel` as
  "client-augmented … `undefined` on the real path", but the server serves it. Widening the client type is
  safe and would let the real inbox card render its decision-first headline today.
- **REQ-050 is partly stale.** It states the list DTO carries neither field, "confirmed against
  `services/bookingRequests/types.ts`" — i.e. confirmed against the client type, not the wire. What the
  wire genuinely lacks is `variantPrice`/`variantPriceUnit` on the *list* row and the `status=answered`
  group filter.
- **`requiredCaregiverGender` is never defaulted or dropped.** `any` is an explicit choice, distinct from
  absent.
- The address the request references is snapshotted at create time; later edits to the saved address do
  not rewrite it.

## Enums

| Vocabulary | Values |
| --- | --- |
| `BookingRequestStatus` | `pending_nurse_response` `accepted_awaiting_payment` `converted` `rejected_by_nurse` `expired_no_response` `payment_deadline_expired` `cancelled_by_customer` |
| `RequiredCaregiverGender` | `male` `female` `any` |
| `RequestRole` *(client-side list filter)* | `customer` `nurse` |

Verified identical to `Baya.Domain/Entities/Booking/BookingRequestStatus.cs`. REQ-015 confirmed these
serialise as the exact snake_case codes.

## Open REQs

| REQ | Status | Effect |
| --- | --- | --- |
| REQ-044 | open | No structured `nurseRejectionReasonCode` — the wire carries free-text `nurseRejectionReason`. The client runs a keyword heuristic over it, documented in-code as a known approximation |
| REQ-050 | open, **narrower than filed** | The list row already has `variantLabel`; it lacks `variantPrice`/`variantPriceUnit`. The `status=answered` group filter is genuinely absent, so the «پاسخ‌داده» tab fires three page-1 queries and concatenates — an unpaged workaround a nurse with many answered requests will hit |
