6.6 KiB
Backend phase 8 report — Booking requests (pre-payment intent)
Date: 2026-07-06 · Track: backend · Depends on: b1 (config/jobs/notifications), b3 (profiles/ patients/tenancy), b4 (addresses), b5 (variants), b7 (search/gender). Unlocks: b9 (bookings/sessions/care) and frontend f7-b8.
What was built
The money-free first half of the engagement lifecycle — the booking_requests table and its full state
machine (request → accept → pay-window → expire/reject/cancel). One additive migration adds a new
booking schema with a single table BookingRequests. No money, no bookings row, no snapshot, no
price anywhere in this phase.
- Domain (
Baya.Domain/Entities/Booking/):BookingRequest(guardedstatuswith a private setter, both deadlines as UTCDateTime, unencryptedCustomerNotes),BookingRequestStatus(7 const codes),BookingRequestTransitions(forward-only edge table +CanTransition),CaregiverGender(male/female/any+Matches). - Application (
Features/Booking/): commandsCreateBookingRequest,AcceptBookingRequest,RejectBookingRequest,CancelBookingRequest,ExpireBookingRequests; queriesListBookingRequests(role-scoped) +GetBookingRequest(party/admin);BookingRequestMapper(stage-1 address masking); DTOs inModels/Booking/;NurseBookingContextinModels/Identity/. - Infrastructure:
BookingRequestConfig(EF config + the 4 covering indexes + soft-delete filter),BookingRequestRepositoryonIUnitOfWork,INurseProfileRepository.GetBookingContextByIdAsync, andBookingRequestExpiryHostedService(recurring sweep, reuses the b1IJobScheduler/BackgroundServiceseam), registered inAddPersistenceServices. - API (
Controllers/V1/):BookingRequestsController(create/accept/{id}/reject/{id}/cancel/{id}/list/get/{id},[Authorize], role/ownership enforced in handlers) +AdminBookingRequestsController(expire,DynamicPermission).
The critical rules, as enforced
- No money / no booking. A request has no price column; accept only sets
payment_deadline_at. Conversion to abookingsrow is b9 (MarkConverted()exists but b8 never calls it). - Two-stage disclosure (stage 1).
customer_notesis unencrypted and the only clinical text the nurse sees; the nurse view/inbox mask the encrypted full address to a coarse city/district. - Tenancy invariant resolved from
ICurrentUser(never the body): patient + address ∈ the caller's customer, variant ∈ the requested nurse — a mismatch is a clean 404. - Same-gender match at request time against
User.Gender; required, never defaulted. - Deadlines frozen from config (
nurse_response_deadline_hoursat create,booking_payment_deadline_minutes= 30 at accept) as absolute UTC timestamps; a later config change can't move them. - Forward-only guard: every write pre-checks
CanTransition→ 409 on an illegal edge; terminal states have no outgoing edge. Accept self-guards against a passed response deadline. The expiry sweep is bounded, paginated, time-injected, and idempotent (theWHERE status = …predicate is the concurrency guard).
What is now testable, and exactly how
Run the API against a reachable SQL Server (dotnet run --project src/API/Baya.Web.Api/...), sign in per role.
The §7 scenarios all pass:
- Create (happy path) — customer
POST /v1/booking_requests/createwith own patient/address, a verified+accepting nurse's active variant, a future date,requiredCaregiverGender: any→200,pending_nurse_response,nurseResponseDeadlineAt = now + nurse_response_deadline_hours,paymentDeadlineAtnull; the nurse gets abooking_request_receivednotification. - Cross-customer patient/address/variant → clean
404, no row created. - Same-gender mismatch (
femalevs amalenurse) →400;anysucceeds. - Accept →
200,accepted_awaiting_payment,paymentDeadlineAt = now + 30 min, customer notified; no bookings row. - Reject with reason →
200,rejected_by_nurse; rejecting a non-pending request →409. - Nurse inbox (
list?role=nurse) showscustomerNotes+ the response countdown, no clinical/encrypted field. - Customer cancels an accepted request →
200,cancelled_by_customer; cancelling a terminal one →409. - Expiry — admin
POST /v1/admin_booking_requests/expire(or the recurring job) moves stale pending →expired_no_responseand stale accepted →payment_deadline_expired, notifies the customer; re-running is a no-op. - Read tenancy — a third party
GET /v1/booking_requests/get/{id}→404.
Automated tests (215 total pass): 14 handler-unit (CreateBookingRequestHandlerTests,
RespondBookingRequestHandlerTests) + the transition-machine tests (BookingRequestTransitionsTests) + 4
DB-backed SQLite tests over the real EF model (BookingRequestExpiryTests, BookingRequestQueryTests via
BookingTestHost) + 5 API integration tests (BookingRequestsApiTests: 401/400/empty-inbox/admin-expire).
dotnet build Baya.sln = 0 new warnings; dotnet test Baya.sln green. Migration applied to the dev DB and the
sweep verified running against SQL Server on boot.
Contracts produced / consumed
- Produced:
dev/contracts/domains/booking-requests.md;swagger.v1.jsonrefreshed (the 7 booking paths + the 3 DTOs). - Consumed: b1 config keys (
nurse_response_deadline_hours,booking_payment_deadline_minutes), b3 profiles/patients + tenancy, b4 addresses, b5 variants (bookable unit;GetOwnedAsynctenancy), b7 nurse gender/matching data.
Nothing is mocked here
This phase owns no third-party integration and introduces no DI seam. It reuses IPlatformConfig,
INotificationDispatcher, IJobScheduler/BackgroundService, IDateTimeProvider, ICurrentUser. There is
no new mocks-registry.md row — the existing IJobScheduler row was updated to note the second hosted job
(the internal expiry sweep is not an external seam).
Follow-ups for b9 (+ b10)
- Consume an
accepted_awaiting_paymentrequest → create thebookingsrow on payment capture (b10), thenrequest.MarkConverted()through the guard. The request↔booking link is 1:1 and b9-owned. - Stage 2: the encrypted
booking_care_instructions(post-confirmation, assigned nurse + admin only). Do not add a clinical field tobooking_requests. - The three-amount money split,
variant_snapshot_json/address_snapshot_json,booking_sessions, EVV, anddispute_window_ends_atare all b9/b10. - Reuse the forward-only status-machine pattern (CONVENTIONS §6) for the
bookingsstate machine.