# دیتابیس — تسک ۰۷

## `resource_occupancy` — مهم‌ترین جدول سیستم

| ستون | نوع | توضیح |
|---|---|---|
| `id` | BIGINT PK AI | BIGINT چون پرحجم‌ترین جدول می‌شود |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `resource_id` | INT NOT NULL | FK → `clinic_resources.id` ON DELETE RESTRICT |
| `appointment_id` | INT NULL | FK → `appointments.id` ON DELETE CASCADE؛ NULL = مسدودسازی دستی |
| `appointment_segment_id` | INT NULL | FK ON DELETE CASCADE |
| `start_at` | INT NOT NULL | **شامل `setup_minutes` منبع** |
| `end_at` | INT NOT NULL | **شامل `cleanup_minutes` منبع** |
| `units` | SMALLINT NOT NULL DEFAULT 1 | |
| `occupancy_kind` | VARCHAR(10) NOT NULL | `exclusive`\|`shared`\|`passive` |
| `status` | VARCHAR(10) NOT NULL | `hold`\|`booked`\|`released` |
| `expires_at` | INT NULL | فقط برای `hold` |
| `created_at` | INT NOT NULL | |

```sql
KEY idx_occ_resource_range (resource_id, start_at, end_at, status)   -- کوئری داغ تسک ۰۶
KEY idx_occ_tenant_range   (entity_type, entity_id, start_at)        -- لیست‌های پنل
KEY idx_occ_appointment    (appointment_id)
KEY idx_occ_expiry         (status, expires_at)                      -- cron انقضا
```

## `resource_occupancy_slot` — تضمین یکتایی

```sql
CREATE TABLE resource_occupancy_slot (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  resource_id INT NOT NULL,
  bucket INT NOT NULL,                 -- floor(timestamp / 300)
  unit_index SMALLINT NOT NULL,        -- 0 .. capacity-1
  occupancy_id BIGINT NOT NULL,
  UNIQUE KEY uniq_occ_slot (resource_id, bucket, unit_index),   -- ← کل تضمین اینجاست
  KEY idx_occ_slot_occupancy (occupancy_id),
  CONSTRAINT fk_occ_slot_occupancy FOREIGN KEY (occupancy_id)
      REFERENCES resource_occupancy(id) ON DELETE CASCADE
) ENGINE=InnoDB;
```

`BUCKET_SECONDS = 300` در `OccupancyWriter::BUCKET_SECONDS` ثابت است. تغییرش بعد از
تولید داده، migration کامل می‌خواهد — در کد کامنت هشدار بگذار.

سطل‌های یک بازه:

```php
// [start, end) نیم‌باز → سطل آخر شامل نمی‌شود اگر دقیقاً روی مرز باشد
$first = intdiv($start, self::BUCKET_SECONDS);
$last  = intdiv($end - 1, self::BUCKET_SECONDS);
```

بدون `-1` نوبت ۱۰:۰۰-۱۰:۳۰ و نوبت ۱۰:۳۰-۱۱:۰۰ سطل مشترک می‌گیرند و دومی بی‌دلیل رد می‌شود.

## `appointment_segments`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `appointment_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `sequence` | SMALLINT NOT NULL | |
| `name` | VARCHAR(150) NOT NULL | snapshot نام بخش در لحظهٔ ثبت |
| `segment_type` | VARCHAR(40) NOT NULL | |
| `start_at` | INT NOT NULL | مطلق |
| `end_at` | INT NOT NULL | |
| `patient_present` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at` | INT NOT NULL | |

```sql
KEY idx_appt_seg_appointment (appointment_id, sequence)
KEY idx_appt_seg_tenant      (entity_type, entity_id, start_at)
```

`name` و `segment_type` عمداً کپی می‌شوند نه FK: قانون پنجم مستند — «هر چیزی که ثبت شد
باید همان‌طور بماند». اگر کلینیک فردا الگو را عوض کند، نوبت دیروز نباید تغییر معنا دهد.

## تغییر `appointments`

```sql
ALTER TABLE appointments
  ADD COLUMN branch_id INT NULL,
  ADD COLUMN plan_total_minutes SMALLINT NULL,
  ADD COLUMN patient_facing_minutes SMALLINT NULL,
  ADD CONSTRAINT fk_appointments_branch FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL,
  ADD KEY idx_appointments_branch (branch_id, slot_start);
```

`slot_start` / `slot_end` **می‌مانند** و در حالت `resource` برابر شروع اولین بخش و پایان
آخرین بخش‌اند. دلیل: `AppointmentRepository`، لیست‌های پنل، سایت عمومی و اپ دسکتاپ همه
روی این دو ستون کوئری می‌زنند. برداشتنشان یعنی بازنویسی همه‌جا.

وضعیت جدید:

```php
public const STATUS_RESCHEDULED = 'rescheduled';
// ALLOWED_TRANSITIONS: confirmed → rescheduled ; pending → rescheduled
```

## Migration

```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```

⚠️ **دو ایندکس را دستی در migration بنویس** — `doctrine:migrations:diff` ترتیب ستون‌های
ایندکس ترکیبی را گاهی متفاوت تولید می‌کند و ترتیب اینجا حیاتی است
(`resource_id` اول در `idx_occ_resource_range`).

## backfill نوبت‌های موجود

```bash
ddev exec php bin/console app:occupancy:backfill --force
```

برای هر نوبت `pending`/`confirmed` آینده در حالت `slot`/`service`:
- یک `appointment_segment` واحد بساز (کل بازه)
- یک `resource_occupancy` روی منبع `type=doctor` همان پزشک
- ردیف‌های `resource_occupancy_slot` متناظر

اگر منبع `type=doctor` وجود ندارد (backfill تسک ۰۲ اجرا نشده)، آن نوبت رد شود و در
خروجی گزارش شود — نه خطا.

بدون این backfill، اولین رزرو در حالت جدید ممکن است روی نوبت قدیمی بنشیند.

## نگهداشت

```bash
ddev exec php bin/console app:occupancy:prune --older-than=90d --force
```

ردیف‌های `resource_occupancy_slot` مربوط به بازه‌های گذشته را حذف می‌کند.
`resource_occupancy` می‌ماند (آدیت و گزارش بهره‌وری تسک ۱۴).

## طبقه‌بندی tenant

| جدول | وضعیت |
|---|---|
| `resource_occupancy` | جفت tenant |
| `appointment_segments` | جفت tenant (uuid ممکن است از request بیاید) |
| `resource_occupancy_slot` | `AGGREGATE_CHILDREN` → ریشه `ResourceOccupancy` — **هرگز مستقیم کوئری نشود** |
