Files
baya-monorepo/archive/build-chain/contracts/domains/booking-requests.md
T
2026-08-02 18:48:32 +03:30

11 KiB

Contract — Booking requests (backend phase b8)

The pre-payment intent layer of the engagement lifecycle: a customer requests a nurse; the nurse accepts/rejects before a config-driven response deadline; on accept a config-driven 30-minute payment window opens. No money and no bookings row exist here — accept only opens the window (conversion is b9/b10). Assumes ../conventions/api-conventions.md + ../conventions/money-and-types.md. Machine schema: ../openapi/swagger.v1.json (refreshed for b8).

Status: live as of backend-phase-b8 · Frontend consumer: frontend-phase-f7-b8

Routing note. Routes are action-style ([controller]/[action], snake_cased). All responses use the standard { succeeded, statusCode, data } envelope; data shapes are below. Request bodies are camelCase.

Key semantics (read first)

  • Two-table split, no money on a request. A booking_requests row never carries a price/total and never creates a booking. Accept moves it to accepted_awaiting_payment and opens the payment window; b9 later consumes that and creates the bookings row (+ money), setting the request to converted.
  • Two-stage clinical disclosure (stage 1). The nurse sees only the unencrypted, limited customer_notes — never a full clinical/care instruction (those are b9's encrypted, post-confirmation booking_care_instructions). The nurse view of a request masks the full address (address line / postal code / recipient) and shows only a coarse city/district location; the customer/admin view returns the full address.
  • Tenancy invariant (enforced at create). The patient and customer_address must belong to the caller's customer; the variant must belong to the requested nurse_id. A mismatch is a clean 404 — never a leak.
  • Same-gender match is first-class. required_caregiver_gender (male/female/any) is matched against the nurse's gender at request time. male/female must equal the nurse's gender; any matches either. A mismatch is a 400. It is required on create and never silently defaulted.
  • Deadlines are frozen from config. nurse_response_deadline_at = create-time now + nurse_response_deadline_hours (default 24h); payment_deadline_at = accept-time now + booking_payment_deadline_minutes (30). Both are absolute UTC timestamps stored on the row — a later config change never moves an existing request's deadlines.
  • Forward-only status machine. Illegal transitions return 409. Terminal states (converted/rejected_by_nurse/expired_no_response/payment_deadline_expired/cancelled_by_customer) have no outgoing edges. An accept after the response deadline, or on a non-pending request, is 409.
  • Auto-expiry. A background sweep (also an admin manual trigger) moves pending_nurse_response → expired_no_response past the response deadline and accepted_awaiting_payment → payment_deadline_expired past the payment window, and notifies the customer.
  • Timestamps are UTC ISO-8601. Ids are BIGINT. There is no money field anywhere in this domain.

Enums used

  • booking_request_status: pending_nurse_response | accepted_awaiting_payment | converted | rejected_by_nurse | expired_no_response | payment_deadline_expired | cancelled_by_customer.
  • required_caregiver_gender: male | female | any.

Endpoints

POST api/v1/booking_requests/create

  • Purpose: a customer requests a nurse for a patient/variant/address/date.
  • Auth: authenticated customer (owner) · Rate-limited: no · Idempotency key: no
  • Request body:
    {
      "nurseId": 42,
      "variantId": 8,
      "patientId": 5,
      "customerAddressId": 6,
      "requestedDate": "2026-08-01",
      "requestedTimeStart": "09:00:00",
      "requestedTimeEnd": "13:00:00",
      "requiredCaregiverGender": "female",
      "customerNotes": "Careful with the IV line."
    }
    
  • Success 200 payload (data): a BookingRequestDto (customer view — full address), status pending_nurse_response, nurseResponseDeadlineAt set, paymentDeadlineAt null.
  • Failure cases: 400 validation (missing/invalid gender, requestedTimeEnd ≤ requestedTimeStart, past date, notes > 1000, inactive variant, nurse not verified/accepting, same-gender mismatch); 401 unauthenticated; 403 not a customer / no customer profile; 404 patient/address not owned or variant not the nurse's or nurse absent.
  • Side effects: in-app booking_request_received notification to the nurse (data_json: booking_request_id, patient_display_name, requested_date).

POST api/v1/booking_requests/accept/{id}

  • Purpose: the assigned nurse accepts a pending request, opening the 30-minute payment window.
  • Auth: authenticated nurse (assigned) · path param id (BIGINT).
  • Request body: none.
  • Success 200 payload (data): BookingRequestDto (nurse view — masked address), status accepted_awaiting_payment, paymentDeadlineAt = now + booking_payment_deadline_minutes (30 min).
  • Failure cases: 401; 403 not a nurse; 404 not the caller's request; 409 not pending / already past the response deadline.
  • Side effects: in-app booking_request_accepted notification to the customer (data_json: booking_request_id, payment_deadline_at). No bookings row and no money are created.

POST api/v1/booking_requests/reject/{id}

  • Purpose: the assigned nurse declines a pending request with a reason.
  • Auth: authenticated nurse (assigned) · path param id.
  • Request body: { "reason": "Fully booked that week." } (required, ≤ 500 chars).
  • Success 200 payload (data): BookingRequestDto, status rejected_by_nurse, nurseRejectionReason set.
  • Failure cases: 400 empty/too-long reason; 401; 403; 404; 409 not pending.
  • Side effects: in-app booking_request_rejected notification to the customer.

POST api/v1/booking_requests/cancel/{id}

  • Purpose: the customer withdraws a request that is still pending_nurse_response or accepted_awaiting_payment (before paying).
  • Auth: authenticated customer (owner) · path param id.
  • Request body: none.
  • Success 200 payload (data): BookingRequestDto (customer view), status cancelled_by_customer.
  • Failure cases: 401; 403; 404 not owned; 409 request is terminal (converted/rejected/expired).

GET api/v1/booking_requests/list

  • Purpose: the role-scoped inbox (paginated).
  • Auth: authenticated (customer or nurse).
  • Query params: status (optional enum filter); role (customer|nurse, optional — disambiguates a user who holds both roles; inferred from the caller's profile when omitted); page/pageSize (default 1 / 50, max 100).
  • Success 200 payload (data): PagedResult<BookingRequestListItemDto> (items, total, page, pageSize). Actionable rows sort first. The customer inbox sets counterpartyName = nurse name + nurseRating; the nurse inbox sets counterpartyName = patient name + customerNotes (stage-1 only).
  • Failure cases: 400 holds both roles and no role given; 401. A caller with neither profile gets an empty page.

GET api/v1/booking_requests/get/{id}

  • Purpose: a single request.
  • Auth: its customer (full address), its nurse (masked address), or an admin (full).
  • Success 200 payload (data): BookingRequestDto.
  • Failure cases: 401; 404 absent or caller is neither party nor admin (existence not leaked).

POST api/v1/admin_booking_requests/expire

  • Purpose: admin/test manual trigger of the expiry sweep (the same command the recurring job runs).
  • Auth: admin (DynamicPermission) · Rate-limited: no.
  • Request body: none.
  • Success 200 payload (data): { "expiredNoResponse": 0, "paymentDeadlineExpired": 0 } — counts moved this run (idempotent; re-running on a drained set returns zeros).
  • Failure cases: 401; 403 non-admin.

Shared shapes

  • BookingRequestDto (full single-request view): id (long), status (enum), nurseId (long), nurseName (string), nurseRating (decimal), nurseTotalReviews (int), patientId (long), patientName (string), variantId (long), variantLabel (string), variantPriceUnit (string), customerAddressId (long), addressTitle (string), cityId (long), cityNameFa/cityNameEn (string), districtId (long?, null = whole city), districtNameFa/districtNameEn (string?), addressLine/postalCode/recipientName/recipientPhone (string?, null in the nurse view — masked), requiredCaregiverGender (enum?), requestedDate (date), requestedTimeStart/requestedTimeEnd (time), customerNotes (string?, stage-1 plaintext), nurseResponseDeadlineAt (UTC datetime), paymentDeadlineAt (UTC datetime?, null until accept), nurseRejectionReason (string?), createdAt (UTC datetime).
  • BookingRequestListItemDto (inbox row): id, status, counterpartyName (nurse name for customer view / patient name for nurse view), nurseRating (decimal?, customer view only), requiredCaregiverGender (enum?), requestedDate, requestedTimeStart, requestedTimeEnd, nurseResponseDeadlineAt, paymentDeadlineAt, customerNotes (string?, nurse view only).
  • ExpireBookingRequestsResult: expiredNoResponse (int), paymentDeadlineExpired (int).

Changelog

  • b8 — initial contract (create/accept/reject/cancel + role-scoped list + single get + admin expire).

Refinement phase 3 additions (REQ-013/014/016/017)

  • BookingRequestDto gains variantPrice (IRR digit-string — the chosen variant's display rate, not an engagement total; the request stays money-free), nurseAvatarUrl (nullable), and bookingId (nullable — the booking created once the request is converted, for the confirmation deep-link).
  • BookingRequestListItemDto gains variantLabel (self-describing inbox row) and patientAge (nullable coarse triage age).
  • GET api/v1/booking_requests/checkout_summary/{id} (owner-scoped) — the C6 money breakdown: { bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit, sessionCount, requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt, serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount }. All IRR digit-strings, computed server-side. Canonical rates: platform_fee_rate = 0.15, vat_rate = 0.10. VAT is carved out of the commission so serviceCostIrr + commissionIrr + vatIrr = totalIrr = gross (the captured amount); commissionIrr is the commission net of VAT, and the raw b10 amounts (grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount) are surfaced alongside.