Domain 5 — Booking & Scheduling
Related: business requirements — Booking & scheduling.
Two distinct phases: the request phase (pre-payment intent) and the booking phase (post-payment commitment). The previous model's biggest domain gap — single-visit-only bookings — is fixed here with booking_sessions, because elder care is dominantly multi-day / live-in.
booking_requests [CORE]
Role: A customer's pre-payment intent for a nurse, service, date, and time. No money involved. Why separate from bookings: a request may be rejected, expire, or have its payment window lapse without a booking ever existing — merging would mean many nullable fields and tangled status logic. The deadlines are computed once and stored so they're immune to later config changes.
| Field | Type | Notes |
|---|---|---|
id, customer_id, nurse_id, patient_id, variant_id, customer_address_id | … | Baseline FKs. Tenancy invariant: patient & address belong to customer_id; variant belongs to nurse_id. |
required_caregiver_gender | NVARCHAR(10) NULL | NEW — male/female/any. Same-gender care is decisive in Iranian bodily-care; surfaced as a first-class filter and matched against nurse gender. |
requested_date, requested_time_start, requested_time_end | … | For multi-day engagements these are the engagement start; sessions carry the per-visit schedule. |
customer_notes | NVARCHAR(1000) NULL | Unencrypted, request-stage only — the only clinical context the nurse sees before accepting (Principle 6). |
status | NVARCHAR(50) | pending_nurse_response → accepted_awaiting_payment → converted / rejected_by_nurse / expired_no_response / payment_deadline_expired / cancelled_by_customer |
nurse_response_deadline_at | DATETIME2 | From config at creation; auto-expire after. |
payment_deadline_at | DATETIME2 NULL | Set on accept; 30-min window. |
nurse_rejection_reason | NVARCHAR(500) NULL | |
| timestamps | … |
Relations: N:1 → customer_profiles, nurse_profiles, patients, nurse_service_variants, customer_addresses; 1:1 → bookings (on conversion).
bookings [CORE]
Role: The confirmed engagement — exists only when the nurse accepted and payment was captured. Source of truth for the service event and its money split. Why snapshots: variant_snapshot_json and address_snapshot_json freeze the service and address at booking time, so later edits/deletes can't corrupt history or disputes.
| Field | Type | Notes |
|---|---|---|
id | BIGINT PK | |
booking_request_id | BIGINT FK UNIQUE | 1:1 — the request that created it |
customer_id, nurse_id, patient_id, variant_id, customer_address_id | … | Denormalized for query performance |
variant_snapshot_json | NVARCHAR(MAX) | Variant + option labels at booking time |
address_snapshot_json | NVARCHAR(MAX) (enc) | Full address at booking time |
partner_center_id | BIGINT FK → partner_centers NULL | NEW — the licensed center legally covering this visit / merchant-of-record |
gross_price_irr | BIGINT | CHANGED (renamed) — total charged to customer |
balinyaar_commission_irr | BIGINT | CHANGED (renamed from platform_fee_amount) — platform's own cut |
platform_fee_rate | DECIMAL(5,4) | Rate snapshot for audit |
nurse_payout_amount | BIGINT | = gross_price_irr − balinyaar_commission_irr. CHECK enforced. |
psp_fee_amount | BIGINT NULL | NEW — gateway cost on this payment, for true margin/reconciliation |
session_count | SMALLINT NOT NULL DEFAULT 1 | NEW — 1 = single visit; >1 = multi-session engagement |
scheduled_date, scheduled_time_start, scheduled_time_end | … | Engagement-level; per-visit lives in booking_sessions |
status | NVARCHAR(30) | pending_payment → confirmed → in_progress → completed → (disputed → closed) / cancelled. Now guarded by an allowed-transition table/CHECK so it can't contradict EVV. |
confirmed_at, cancelled_at, cancellation_reason, cancelled_by, completed_at | … | |
dispute_window_ends_at | DATETIME2 NULL | NEW — completed_at + config(dispute_window_hours, default 72). Payout eligible only after this passes with no open dispute. |
payout_released | — | CUT — second source of truth for "paid"; now derived from a nurse_payout_booking_links row + ledger. |
| timestamps | … |
CHECK: gross_price_irr = balinyaar_commission_irr + nurse_payout_amount; all amounts ≥ 0. Relations: 1:1 ← booking_requests; 1:N → booking_sessions, payment_transactions, ledger_entries; 1:1 → booking_care_instructions, visit_verifications (see note), reviews, invoices; referenced by nurse_payout_booking_links, refunds, nurse_clawbacks.
booking_sessions [MVP] — NEW
Role: One row per visit within a booking. A 10-day post-op package or a month of nightly شبانهروزی care is one booking with N sessions, each with its own schedule, EVV, and payout eligibility. Why: the single-visit model literally cannot represent the dominant elder-care engagement; and money for a long engagement must release per completed session, not as one whole-month escrow held for weeks. Mid-engagement cancellation then cleanly refunds only the un-started sessions.
| Field | Type | Notes |
|---|---|---|
id | BIGINT PK | |
booking_id | BIGINT FK → bookings | |
session_index | INT | 1-based ordinal |
scheduled_date, scheduled_time_start, scheduled_time_end | … | Per-visit |
visit_payout_amount | BIGINT | Portion of nurse_payout_amount for this session |
status | NVARCHAR(20) | scheduled / in_progress / completed / missed / cancelled |
payout_eligible_at | DATETIME2 NULL | Per-session dispute-window close |
cancellation_event_id | BIGINT NULL | If this session was cancelled |
| timestamps | … |
Relations: N:1 → bookings; 1:1 → visit_verifications. Note: for a single-visit booking, exactly one session is created so the EVV/payout path is uniform.
booking_care_instructions [CORE]
Role: Encrypted clinical/logistical context provided at booking time, visible only post-confirmation to the assigned nurse and admin. Why separate + encrypted: keeps the financial/scheduling table clean and enforces the two-stage disclosure boundary (Principle 6) with stricter access controls. Fields unchanged (current conditions, medications, allergies, special instructions, emergency contact — all enc). Relations: 1:1 → bookings.
visit_verifications [CORE]
Role: Electronic Visit Verification — the authoritative record that a visit happened and for how long; required for payout. Why: in-home care is unobserved; GPS + timestamped check-in/out is the proof that safely releases escrow. check_in_address_match is advisory (a mismatch triggers admin review, not auto-cancel). CHANGED: the FK moves to booking_session_id so each visit in a multi-session engagement is verified independently; the mapping between visit_verifications.status and the parent bookings.status is documented so the two state machines cannot silently diverge. Fields otherwise unchanged. Relations: 1:1 → booking_sessions.
cancellation_policies [MVP] — NEW
Role: Config-driven, snapshot-able cancellation/refund tiers by lead time and initiating actor. Why: "default 100% refund" is naive — a free cancel 24h ahead, a 50% charge inside 24h, and a nurse-no-show full refund + nurse penalty are different money flows, and the applicable rule must be frozen onto the booking at cancel time (the same snapshot discipline used for platform_fee_rate).
| Field | Type | Notes |
|---|---|---|
id | BIGINT PK | |
code | NVARCHAR(50) UNIQUE | e.g. standard_24h |
applies_to | NVARCHAR(20) | customer / nurse / admin |
hours_before_start_min, hours_before_start_max | INT NULL | Tier bounds |
refund_percentage | DECIMAL(5,2) | 0–100 |
fee_amount_or_rate | … | Cancellation fee / nurse penalty |
is_active, timestamps | … |
Relations: referenced (snapshot) by refunds and the cancellation_event recorded on a session/booking.