11 KiB
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
bookingsrow 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;datashapes are below. Request bodies are camelCase.
Key semantics (read first)
- Two-table split, no money on a request. A
booking_requestsrow never carries a price/total and never creates a booking. Accept moves it toaccepted_awaiting_paymentand opens the payment window; b9 later consumes that and creates thebookingsrow (+ money), setting the request toconverted. - 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-confirmationbooking_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
patientandcustomer_addressmust belong to the caller's customer; thevariantmust belong to the requestednurse_id. A mismatch is a clean404— 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/femalemust equal the nurse's gender;anymatches either. A mismatch is a400. It is required on create and never silently defaulted. - Deadlines are frozen from config.
nurse_response_deadline_at= create-timenow + nurse_response_deadline_hours(default 24h);payment_deadline_at= accept-timenow + 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, is409. - Auto-expiry. A background sweep (also an admin manual trigger) moves
pending_nurse_response → expired_no_responsepast the response deadline andaccepted_awaiting_payment → payment_deadline_expiredpast 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
200payload (data): aBookingRequestDto(customer view — full address), statuspending_nurse_response,nurseResponseDeadlineAtset,paymentDeadlineAtnull. - Failure cases:
400validation (missing/invalid gender,requestedTimeEnd ≤ requestedTimeStart, past date, notes > 1000, inactive variant, nurse not verified/accepting, same-gender mismatch);401unauthenticated;403not a customer / no customer profile;404patient/address not owned or variant not the nurse's or nurse absent. - Side effects: in-app
booking_request_receivednotification 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
200payload (data):BookingRequestDto(nurse view — masked address), statusaccepted_awaiting_payment,paymentDeadlineAt = now + booking_payment_deadline_minutes(30 min). - Failure cases:
401;403not a nurse;404not the caller's request;409not pending / already past the response deadline. - Side effects: in-app
booking_request_acceptednotification to the customer (data_json:booking_request_id,payment_deadline_at). Nobookingsrow 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
200payload (data):BookingRequestDto, statusrejected_by_nurse,nurseRejectionReasonset. - Failure cases:
400empty/too-long reason;401;403;404;409not pending. - Side effects: in-app
booking_request_rejectednotification to the customer.
POST api/v1/booking_requests/cancel/{id}
- Purpose: the customer withdraws a request that is still
pending_nurse_responseoraccepted_awaiting_payment(before paying). - Auth: authenticated customer (owner) · path param
id. - Request body: none.
- Success
200payload (data):BookingRequestDto(customer view), statuscancelled_by_customer. - Failure cases:
401;403;404not owned;409request 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
200payload (data):PagedResult<BookingRequestListItemDto>(items,total,page,pageSize). Actionable rows sort first. The customer inbox setscounterpartyName= nurse name +nurseRating; the nurse inbox setscounterpartyName= patient name +customerNotes(stage-1 only). - Failure cases:
400holds both roles and norolegiven;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
200payload (data):BookingRequestDto. - Failure cases:
401;404absent 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
200payload (data):{ "expiredNoResponse": 0, "paymentDeadlineExpired": 0 }— counts moved this run (idempotent; re-running on a drained set returns zeros). - Failure cases:
401;403non-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)
BookingRequestDtogainsvariantPrice(IRR digit-string — the chosen variant's display rate, not an engagement total; the request stays money-free),nurseAvatarUrl(nullable), andbookingId(nullable — the booking created once the request isconverted, for the confirmation deep-link).BookingRequestListItemDtogainsvariantLabel(self-describing inbox row) andpatientAge(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 soserviceCostIrr + commissionIrr + vatIrr = totalIrr = gross(the captured amount);commissionIrris the commission net of VAT, and the raw b10 amounts (grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount) are surfaced alongside.