# Domain 5 — Booking & Scheduling

← Database Model

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.

↑ Back to top