Files
baya-monorepo/archive/docs/flows/booking-request.md
T
2026-08-02 20:01:31 +03:30

14 KiB

Flow — booking-request

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer (C4/C5) + nurse (inbox) · Status: partial Client: real · Server: real Business source: product/business/05-booking-and-scheduling.md Integration: docs/integration/domains/booking-requests.md

What it does

Stage one of booking, and the only stage with no money in it. A family picks a nurse from search, names the patient, the service variant, the address, a date/time window and the caregiver gender they need, and sends a request. The nurse has a frozen response window to accept or decline; an accept opens a 30-minute payment window and hands the customer to checkout. A booking_requests row becomes a bookings row only on payment capture.

Screens

Step Route Component / notes
C4 — request form /fa/bookings/request?nurse_id=&variant_id=&required_gender= client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx (727 ln). Waits for patients + addresses + nurse profile before mounting so defaultValues are the only values. JalaliDateIntentPicker + three preset time windows (TIME_WINDOWS, :55) or custom. Blocks a same-gender mismatch before the round-trip: genderMismatch at :216, submit guard :248, inline alert :544
C5 — awaiting response /fa/bookings/request/[id] …/bookings/request/[id]/page.tsx (368 ln). Polls every 15 s while non-terminal, stops on terminal (useBookingRequest, hook :20-23). StepperHeader 3-step tracker, BookingRequestSummaryCard, CountdownTimer. Each of the five terminal statuses gets its own recovery card
customer inbox /fa/bookings …/(customer)/bookings/BookingsScreen.tsx:53 mounts useCustomerRequests() and deep-links rows back to C5. There is no dedicated customer requests page — this list is the only entry point back into C5
nurse inbox /fa/nurse/requests …/nurse/requests/page.tsx (198 ln). Three tabs «در انتظار» / «پاسخ‌داده» / «منقضی»; urgency-tinted countdown pill (teal >2 h, amber <2 h, terracotta <30 min, :17-18); shows customerNotes only — never an address
nurse detail /fa/nurse/requests/[id] …/nurse/requests/[id]/page.tsx (343 ln). Accept (confirm dialog) / reject (reason dialog, ≤500 chars). A stale action's 409 becomes the «action_stale» toast + refetch (:82-89)

Client service seam is real: USE_BOOKING_REQUESTS_MOCK = false (constants.ts:14). Its doc-comment above that line still argues for the mock and is stale.

API

The six client-facing ops are wired and were traced client → server; checkout_summary sits on this controller but is consumed by the checkout flow, and expire has no client caller at all. Shapes live in docs/integration/domains/booking-requests.md.

Call Endpoint Notes
C4 submit POST /booking_requests/create clientApi.ts:27BookingRequestsController.cs:34CreateBookingRequestCommand.Handler.cs
C5 / nurse detail GET /booking_requests/get/{id} clientApi.ts:35 → controller :59. Role-scoped masking in the mapper, not the client
both inboxes GET /booking_requests/list?role=&status=&page=&pageSize= clientApi.ts:37 → controller :54. Role resolution ListBookingRequestsQuery.Handler.cs:45-58
nurse accept POST /booking_requests/accept/{id} clientApi.ts:48 → controller :44
nurse reject POST /booking_requests/reject/{id} clientApi.ts:51 → controller :49; id comes from the route, reason from the body
customer cancel POST /booking_requests/cancel/{id} clientApi.ts:59 → controller :39
checkout money GET /booking_requests/checkout_summary/{id} on this controller, consumed by checkout-and-payment
expiry sweep POST /admin_booking_requests/expire AdminBookingRequestsController.cs:22 is [Authorize(DynamicPermission)]403 for both demo admins (re-checked live: …020 and …030 both 403 "Authorization Error"). The in-process booking_request_expiry job (BookingRequestExpiryJob.cs, 1-min interval) runs it unattended anyway

Rules that must hold

Rule Where enforced Live evidence
Response deadline frozen from config, never recomputed (05-booking §a.2) CreateBookingRequestCommand.Handler.cs:72,86nurse_response_deadline_hours created 2026-08-02T12:56:14 → deadline 2026-08-03T12:56:14 = 24 h
30-minute payment window on accept (05-booking §a.3) AcceptBookingRequestCommand.Handler.cs:49-50booking_payment_deadline_minutes accepted 12:56:42paymentDeadlineAt 13:26:42 = 30 min
Forward-only status; a sideways move is a clean 409, never a 500 AcceptBookingRequestCommand.Handler.cs:38-39; BookingRequestTransitions behind BookingRequest.cs:66 second accept on req 21 → 409 "This request can no longer be accepted."; second reject on req 23 → 409 "This request can no longer be rejected."
Stage-1 disclosure: nurse gets customerNotes + coarse city/district only BookingRequestMapper.cs:35-38 (includeFullAddress) nurse view of req 21: addressLine/postalCode/recipientName/recipientPhone all null; cityNameFa/districtNameFa present. Customer's own view: full address
Tenancy — patient + address ∈ caller, variant ∈ nurse; a mismatch is 404 not a leak CreateBookingRequestCommand.Handler.cs:37-47; GetTrackedForNurseAsync nurse 2 reading nurse 1's request 21 → 404
Same-gender care is decisive, never defaulted client page.tsx:216,248, server CreateBookingRequestCommand.Handler.cs:61-64 (CaregiverGender.Matches)
Nurse must be verified and accepting bookings CreateBookingRequestCommand.Handler.cs:56-57
customerNotes ≤ 1000, nurseRejectionReason ≤ 500 CreateBookingRequestCommand.Validator.cs:25, RejectBookingRequestCommand.Validator.cs:10-12, mirrored client-side in constants.ts:31,34
Countdown renders a server-frozen deadline, never a locally computed one CountdownTimer.tsx:84-85 true, but see the timezone gap below

How to test

Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md for boot, the Seams__Sms__Provider=mock override, and dev/last_otp.

The seeded world cannot exercise accept/reject. It was seeded 2026-07-26; every seeded request has aged into a terminal state. The seeded baseline is 11 rows for nurse 1 (converted 6, payment_deadline_expired 3, rejected_by_nurse 1, expired_no_response 1) and 9 for customer …010 — zero pending_nurse_response, zero accepted_awaiting_payment. So /fa/nurse/requests opens on an empty «در انتظار» tab and the accept/reject buttons are unreachable on seeded data. (The live inbox now reads 14 / 12 because the probe rows below — 21 accepted, 22 cancelled, 23 rejected — were left in place.) Create your own:

  1. As …010 go to /fa/search, open نرس زهرا عزیزی (nurse 1, female, verified), press «درخواست رزرو».
  2. On C4 pick patient حسن محمدی, service «مراقبت ساعتی سالمند» (variant 2, 250 000 IRR/hr), address «خانه», a future date, a time window, gender «زن». Submit. Expect: redirect to /fa/bookings/request/<newId>, step 2 of 3 active, status «در انتظار تایید پرستار», a countdown against a deadline ~24 h out.
  3. In another browser profile log in as 09120000001 (nurse 1) and open /fa/nurse/requests. Expect: the new request in «در انتظار» with the service headline, the patient name, the gender chip, your note — and no address. Open it: the location line reads «تهران · منطقه ۳» only.
  4. Press «پذیرش». Expect: 200, status flips to accepted_awaiting_payment, the row leaves «در انتظار», and the customer's C5 (polling at 15 s) shows the «پذیرفته شد» badge, a 30-minute countdown and «ادامه پرداخت».
  5. Press «پذیرش» again (or reload and retry): expect a 409 and the «action_stale» toast.
  6. For the reject leg, create a second request and press «رد درخواست» with a reason (≤500 chars). Expect: 200, status rejected_by_nurse, the reason echoed back on the DTO, and C5 swapping to the rejection recovery card. A repeat reject is 409 "This request can no longer be rejected."

Verified by curl on 2026-08-02: request 21 created → nurse-1 pending inbox total 1 → accepted (paymentDeadlineAt +30 min) → second accept 409; request 22 created then cancelled by the customer (cancelled_by_customer, 200); request 23 created → rejected by nurse 1 (200, rejected_by_nurse, nurseRejectionReason echoed) → second reject 409. Tenancy re-checked live: nurse 2 reading request 21 → 404. Every leg of this flow is now live-walked, not code-traced.

Re-walked independently the same day on a fresh row: request 24 created by …010 against nurse 1 (200, pending_nurse_response, nurseResponseDeadlineAt = createdAt + 24 h exactly) → accepted by nurse 1 (200, accepted_awaiting_payment, paymentDeadlineAt = accept instant + 30 min exactly, addressLine/recipientPhone null in the nurse's own accept response) → second accept 409 → accept by nurse 2 404. Same results, so the four load-bearing behaviours above are reproducible, not one-off.

curl gotcha: sending non-ASCII customerNotes as a -d argument from Git Bash produced a spurious 400. Put the JSON in a UTF-8 file and use --data-binary @file; it then returns 200 with the note intact. This is a shell encoding artifact, not a server defect.

Known gaps

  • Deadline timestamps lose their timezone, so every countdown on this flow is wrong off-UTC. BookingRequestDto.cs:39-40 and BookingRequestListItemDto.cs:19-20 type the two deadlines as DateTime (Kind unspecified after the EF round-trip), so the wire carries "2026-08-03T12:56:14.1701328" with no Z. CountdownTimer.tsx:84 calls Date.parse on it, which per spec reads an offset-less date-time as local. In Tehran (UTC+3:30) the customer's 30-minute payment window renders as ~4 hours and the request expires while the timer still shows time left. createdAt is a DateTimeOffset and is correct, so windowStart and deadlineIso on the same progress ring disagree by the UTC offset.
  • Nothing seeded is actionable. No pending/accepted request survives, so the nurse inbox, the accept path, the reject path and the payment-window handoff are all unwalkable without creating a fresh request (see above). Cause: DemoLifecycleSeeder anchors to first-run epoch, not wall clock.
  • The expire admin endpoint is unreachable for the demo adminsAdminBookingRequestsController.cs:22 uses DynamicPermission, which super_admin/finance do not satisfy. Only the background booking_request_expiry job can move a stale request; a tester cannot force expiry on demand.
  • The «پاسخ‌داده» tab is unpaged. The API filters one status at a time and there is no status-group filter, so nurse/requests/page.tsx:37-39 fires three separate page-1 queries and concatenates them (Pager is hidden for that tab). A nurse with >20 accepted/converted/rejected requests silently loses rows. This is the surviving half of REQ-050.
  • No structured rejection-reason code (REQ-044). The wire carries free-text nurseRejectionReason, so C5 runs a keyword heuristic (RETRY_BLOCK_KEYWORDS, bookings/request/[id]/page.tsx:23-28 — seven strings: gender/coverage/area/«جنسیت»/«پوشش»/«منطقه»/«محدوده») to decide whether to offer «درخواست دوباره از همین پرستار». Any other wording defeats it — the live probe's "schedule conflict" passed through.
  • services/bookingRequests/types.ts is behind the wire. variantLabel is typed string | null optional on the list item (types.ts:137) and documented as "client-augmented", but the server always serves it — confirmed live. nurse/requests/page.tsx:135 therefore keeps a hasPricedService fallback branch that is dead on the real path.
  • The list row still lacks variantPrice/variantPriceUnit, so the nurse inbox card cannot show the money she is being asked to commit to without opening the detail.
  • patientAge is served but never modelled client-side. BookingRequestListItemDto.cs:25 returns it and the wire carries it (live: "patientAge": 78 on every nurse-inbox row), but BookingRequestListItem (client/src/services/bookingRequests/types.ts:118-140) declares no such field — patientAge appears nowhere in client/src except a comment in apis/clientApi.ts:23. REQ-014's triage age therefore never reaches the inbox card, and widening the client type is a pure-client fix.
  • useCustomerRequests does not poll (no refetchInterval, useCustomerRequests.ts:21-27) while the detail hook does, so the pending-request countdowns on /fa/bookings go stale until a manual refresh.
  • useBookingRequest has no useIsAuthenticated gate (unlike both list hooks), so a hard reload of C5 before hydration can fire an unauthenticated GET and flash the error card.
  • nurseAvatarUrl is served as a file:///C:/Users/… local disk path (verified live on requests 13, 21, 22). C5 does not pass it to BookingRequestSummaryCard, so this flow is unaffected — but checkout renders it and will show a broken avatar.
  • The nurse sees the patient's display name pre-accept (patientName, verified live). That is wider than the "notes + coarse address only" wording in the integration doc; confirm it is the intended stage-1 boundary rather than a leak.