Files
baya-monorepo/dev/shared-working-context/reports/backend-phase-8-report.md
T
2026-07-06 02:48:56 +03:30

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 (guarded status with a private setter, both deadlines as UTC DateTime, unencrypted CustomerNotes), BookingRequestStatus (7 const codes), BookingRequestTransitions (forward-only edge table + CanTransition), CaregiverGender (male/female/ any + Matches).
  • Application (Features/Booking/): commands CreateBookingRequest, AcceptBookingRequest, RejectBookingRequest, CancelBookingRequest, ExpireBookingRequests; queries ListBookingRequests (role-scoped) + GetBookingRequest (party/admin); BookingRequestMapper (stage-1 address masking); DTOs in Models/Booking/; NurseBookingContext in Models/Identity/.
  • Infrastructure: BookingRequestConfig (EF config + the 4 covering indexes + soft-delete filter), BookingRequestRepository on IUnitOfWork, INurseProfileRepository.GetBookingContextByIdAsync, and BookingRequestExpiryHostedService (recurring sweep, reuses the b1 IJobScheduler/BackgroundService seam), registered in AddPersistenceServices.
  • 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 a bookings row is b9 (MarkConverted() exists but b8 never calls it).
  • Two-stage disclosure (stage 1). customer_notes is 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_hours at 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 (the WHERE 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:

  1. Create (happy path) — customer POST /v1/booking_requests/create with own patient/address, a verified+accepting nurse's active variant, a future date, requiredCaregiverGender: any200, pending_nurse_response, nurseResponseDeadlineAt = now + nurse_response_deadline_hours, paymentDeadlineAt null; the nurse gets a booking_request_received notification.
  2. Cross-customer patient/address/variant → clean 404, no row created.
  3. Same-gender mismatch (female vs a male nurse) → 400; any succeeds.
  4. Accept200, accepted_awaiting_payment, paymentDeadlineAt = now + 30 min, customer notified; no bookings row.
  5. Reject with reason → 200, rejected_by_nurse; rejecting a non-pending request → 409.
  6. Nurse inbox (list?role=nurse) shows customerNotes + the response countdown, no clinical/encrypted field.
  7. Customer cancels an accepted request → 200, cancelled_by_customer; cancelling a terminal one → 409.
  8. Expiry — admin POST /v1/admin_booking_requests/expire (or the recurring job) moves stale pending → expired_no_response and stale accepted → payment_deadline_expired, notifies the customer; re-running is a no-op.
  9. 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.json refreshed (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; GetOwnedAsync tenancy), 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_payment request → create the bookings row on payment capture (b10), then request.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 to booking_requests.
  • The three-amount money split, variant_snapshot_json/address_snapshot_json, booking_sessions, EVV, and dispute_window_ends_at are all b9/b10.
  • Reuse the forward-only status-machine pattern (CONVENTIONS §6) for the bookings state machine.