Files
baya-monorepo/product/data-model/05-booking-and-scheduling.md
2026-06-24 01:32:46 +03:30

89 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) | 0100 |
| `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.