# Model: Booking

**Table**: `clinic_bookings` (new)

## Purpose

The hub of the whole module. Represents one patient's appointment (in-clinic visit or online
video consultation) with one doctor at one branch, at one time slot.

## Fields

| Column | Type | Constraints | Notes |
|---|---|---|---|
| `id` | bigint | PK | |
| `patient_id` | bigint | `foreignId('patient_id')->constrained('users')->restrictOnDelete()` | the `User`, not `PatientProfile` directly — profile is looked up via the relation |
| `doctor_id` | bigint | `foreignId('doctor_id')->constrained('clinic_doctors')->restrictOnDelete()` | |
| `branch_id` | bigint | `foreignId('branch_id')->constrained('branches')->restrictOnDelete()` | stored directly (not derived from doctor) since a doctor can work at several branches |
| `type` | string | `enum`-like via string + validation | `visit` / `online` |
| `scheduled_at` | datetime | | start of the slot |
| `duration_minutes` | unsignedSmallInteger | | copied from `clinic_doctor_branch.slot_duration_minutes` at booking time, so later price/duration changes don't retroactively alter past bookings |
| `price` | decimal(10,2) | | copied from `clinic_doctor_branch.visit_price` or `.consultation_price` at booking time, same reasoning |
| `status` | string | default `'pending'` | `pending` → `confirmed` → `completed` / `cancelled` |
| `cancelled_at` | datetime | nullable | |
| `cancel_reason` | string | nullable | |
| `rescheduled_from_id` | bigint | `foreignId('rescheduled_from_id')->nullable()->constrained('clinic_bookings')->nullOnDelete()` | self-referencing, links a reschedule to its original booking |
| `created_by` | bigint | `foreignId(...)->nullable()->constrained('admins')->nullOnDelete()` | null if the patient booked it themselves; set if a Receptionist booked it manually |
| `created_at` / `updated_at` | timestamp | | |

**No soft deletes** — a cancelled booking is a status, not a deletion (matches the "transactional
tables don't soft-delete by default" convention seen on `sales_orders`).

## Relationships

| Relation | Type | Target |
|---|---|---|
| `patient()` | `belongsTo` | `User` |
| `doctor()` | `belongsTo` | `Doctor` |
| `branch()` | `belongsTo` | `Core\Branch` |
| `payment()` | `hasOne` | `Payment` |
| `medicalRecord()` | `hasOne` | `MedicalRecord` |
| `review()` | `hasOne` | `Review` |
| `rescheduledFrom()` | `belongsTo` (self) | `Booking` |

## Status lifecycle (see `flows/02-booking-flow.md` for the full sequence)

`pending` (created, awaiting payment) → `confirmed` (payment succeeded, per `flows/03-payment-flow.md`)
→ `completed` (visit attended / consultation finished) or `cancelled` (by patient within policy,
or by Clinic Admin/Receptionist).

## Decisions

- `price` and `duration_minutes` are **copied at booking time**, not looked up live from
  `clinic_doctor_branch` — this is the standard "snapshot the price at time of transaction"
  pattern already used elsewhere in the ERP for order/invoice lines, so a later price change
  never alters an already-placed booking's cost.
- Cancellation/reschedule time-window policy (24h / 12h defaults from the brief) is enforced in
  `BookingService`, reading a configurable value from `clinic.policies.*` (Super Admin-set
  default, optionally overridden per clinic) — not hard-coded.
