backend phase 8

This commit is contained in:
hamid
2026-07-06 02:48:56 +03:30
parent 99ebf5d881
commit 2cfc082a04
55 changed files with 7480 additions and 7 deletions
@@ -12,6 +12,34 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
- **Notes for frontend:** <anything load-bearing>
-->
## backend-phase-8 — Booking requests (pre-payment intent) — 2026-07-06
- **Shipped:** the money-free request lifecycle via one additive migration — new **`booking`** schema,
**1 table** `BookingRequests`. The customer-side (`CreateBookingRequest` — tenancy invariant
patient+address∈customer & variant∈nurse, bookability + **same-gender** match, response deadline frozen from
`nurse_response_deadline_hours`; `CancelBookingRequest`), the nurse-side (`AcceptBookingRequest` — freezes
`payment_deadline_at` from `booking_payment_deadline_minutes`=30, self-guards the response deadline;
`RejectBookingRequest` — reason required), the reads (`ListBookingRequests` role-scoped inbox +
`GetBookingRequest` party/admin, **nurse view masks the full address, exposes only `customer_notes`**), and
the **forward-only status guard** (`BookingRequestTransitions`) + the idempotent, paginated
`ExpireBookingRequests` sweep behind a recurring `BackgroundService` (`BookingRequestExpiryHostedService`,
reuses the b1 `IJobScheduler` seam) + an admin manual trigger. **2 controllers:** `BookingRequestsController`
(customer/nurse) + `AdminBookingRequestsController` (`POST admin_booking_requests/expire`). Inbox/expiry
covering indexes `(nurse_id,status)`/`(customer_id,status)`/`(status,response_deadline)`/`(status,payment_deadline)`.
**No money column, no `bookings` row, no snapshot, no price** anywhere.
- **Contracts:** dev/contracts/domains/booking-requests.md + openapi snapshot refreshed (yes — the 7 booking
paths + `BookingRequestDto`/`BookingRequestListItemDto`/`ExpireBookingRequestsResult`).
- **Mocked:** **nothing new** — reuses `IPlatformConfig`/`INotificationDispatcher`/`IJobScheduler`/
`IDateTimeProvider`/`ICurrentUser`. **No new `mocks-registry.md` row** (the `IJobScheduler` row was updated to
note the second hosted job).
- **Gate:** build clean (0 new code warnings) / tests green (215 pass: +20 booking = 14 handler-unit + the
transition machine + 4 DB-backed SQLite + 5 API integration). Migration applied to the dev DB; API boots with
all booking paths in Swagger; the expiry sweep runs on startup (verified against SQL Server).
- **Handoff:** backend/handoff/after-backend-phase-8.md
- **Notes for frontend:** f7-b8 = the request form (C4 → `booking_requests/create`), the status tracker (C5 →
`booking_requests/get/{id}` + customer inbox with the response + 30-min payment countdowns + `cancel`), and
the nurse inbox (`booking_requests/list?role=nurse` + `accept`/`reject`). Gender required (`male`/`female`/
`any`); deadlines are server-frozen UTC timestamps; the nurse sees only `customer_notes` + a masked address.
## backend-phase-7 — Search & matching (nurse search index) — 2026-07-05
- **Shipped:** the discovery layer via one additive migration — new **`search`** schema, **1 table**
`NurseSearchIndices` (the denormalized `nurse_search_index`): **one flat row per (bookable variant ×
@@ -0,0 +1,68 @@
# Handoff — after backend-phase-8 (Booking requests · pre-payment intent)
**The booking-request lifecycle is live end-to-end.** A customer can request a nurse; the nurse accepts
(opening a config-driven 30-minute payment window) or rejects; both sides read their role-scoped inbox and a
single request; unanswered/unpaid requests auto-expire on a recurring sweep (also an admin manual trigger).
**No money and no `bookings` row exist yet** — that conversion is b9/b10.
## What b9 must consume (the next backend phase)
- **Convert an `accepted_awaiting_payment` request → a `bookings` row** once payment is captured (b10), then
set the request to `converted` **through the `BookingRequestTransitions` guard** (`request.MarkConverted()`
already exists and is the only sanctioned path; b8 never writes this edge itself). The request↔booking link
is 1:1 and b9-owned.
- **b8 deliberately exposes only `customer_notes` (stage 1).** The full **encrypted** clinical/care
instructions are b9's **stage 2** (`booking_care_instructions`, readable only post-confirmation by the
assigned nurse + admin). Do **not** add an encrypted clinical field to `booking_requests`.
- **The money split, snapshots, sessions, EVV, dispute window** are all b9/b10 — b8 persists **no**
`variant_snapshot_json`/`address_snapshot_json`, no price, no ledger entry. The b5 `IVariantSnapshotSerializer`
is still the tool for the booking snapshot at conversion time.
- **Reuse the forward-only status-machine pattern** for the `bookings` state machine (see CONVENTIONS §6 —
"Forward-only status machine"). Same shape: const codes + a static `CanTransition` edge table + entity-owned
transitions + handler pre-check → 409.
## What f7-b8 can now build (frontend)
All routes are **action-style** (`[controller]/[action]`, snake_case) with the standard
`{ succeeded, statusCode, data }` envelope. Full shapes in
[`dev/contracts/domains/booking-requests.md`](../../contracts/domains/booking-requests.md); types come from the
refreshed `swagger.v1.json`**do not guess**.
- **Request form (C4)** → `POST api/v1/booking_requests/create` (customer). Body: `nurseId`, `variantId`,
`patientId`, `customerAddressId`, `requestedDate` (`YYYY-MM-DD`), `requestedTimeStart`/`requestedTimeEnd`
(`HH:mm:ss`), `requiredCaregiverGender` (`male`|`female`|`any`, **required**), `customerNotes` (≤ 1000, the
**only** clinical text the nurse sees). Inputs come from search (nurse+variant), patients (b3), addresses (b4).
- **Awaiting-acceptance / status tracker (C5)** → `GET api/v1/booking_requests/get/{id}` + the customer inbox
`GET api/v1/booking_requests/list?role=customer`. Show two countdowns: to `nurseResponseDeadlineAt` (pending)
and to `paymentDeadlineAt` (accepted — the 30-min window). Customer can `POST …/cancel/{id}` while pending or
accepted-awaiting-payment.
- **Nurse incoming-requests inbox** → `GET api/v1/booking_requests/list?role=nurse` (+ `status=` filter) with
`POST …/accept/{id}` and `POST …/reject/{id}` (reason required). The nurse row shows `counterpartyName` =
patient name + `customerNotes` only, plus the response countdown — **never** a full address or any clinical
field beyond `customerNotes`.
## Rules the UI must respect
- **Same-gender is first-class and required.** Send `requiredCaregiverGender` explicitly (never default it);
`male`/`female` must match the nurse's gender or the create is a `400`. `any` matches either.
- **Deadlines are server-frozen absolute UTC timestamps.** Render countdowns from them; never recompute a
deadline client-side. `paymentDeadlineAt` is null until the nurse accepts.
- **Two-stage disclosure.** The nurse detail view returns a **masked** address (city/district only,
`addressLine`/`postalCode`/recipient are null); the customer/admin view returns the full address. Do not
expect (or request) clinical instructions here — they don't exist until b9.
- **Status drives the UI.** Terminal states (`converted`/`rejected_by_nurse`/`expired_no_response`/
`payment_deadline_expired`/`cancelled_by_customer`) allow no further actions; a stale action returns `409`.
- **`get/{id}` is party-scoped.** A non-party caller gets `404` (existence not leaked).
## Contracts
- New: [`dev/contracts/domains/booking-requests.md`](../../contracts/domains/booking-requests.md).
- `swagger.v1.json` refreshed (adds `booking_requests/{create,accept,reject,cancel,list,get}` +
`admin_booking_requests/expire` + `BookingRequestDto`/`BookingRequestListItemDto`/`ExpireBookingRequestsResult`).
## Nothing new is mocked
b8 introduces **no** external seam and adds **no** new `mocks-registry.md` row. It reuses `IPlatformConfig`,
`INotificationDispatcher`, `IJobScheduler`/`BackgroundService`, `IDateTimeProvider`, `ICurrentUser`. The
expiry sweep is an internal hosted service (the registry's `IJobScheduler` row was updated to note the second
job), not an external integration.