# Domain 5 — Booking & Scheduling

[← Database Model](index.md)

**Related:** business requirements — [Booking & scheduling](../business/05-booking-and-scheduling.md).

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.
