# دیتابیس — تسک ۱۲

## `course_protocols`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `service_item_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `session_count` | SMALLINT NOT NULL | |
| `min_days` | SMALLINT NOT NULL | |
| `ideal_days` | SMALLINT NOT NULL | |
| `max_days` | SMALLINT NOT NULL | |
| `prefer_same_resource` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
UNIQUE KEY uniq_protocol_service (service_item_id)     -- یک پروتکل فعال per سرویس
KEY idx_protocols_tenant (entity_type, entity_id, active)
```

قید اپلیکیشنی: `min_days <= ideal_days <= max_days` و `session_count >= 2`
(دورهٔ یک‌جلسه‌ای همان نوبت تکی است).

## `course_protocol_steps`

```sql
CREATE TABLE course_protocol_steps (
  id INT PRIMARY KEY AUTO_INCREMENT,
  protocol_id INT NOT NULL,
  session_number SMALLINT NOT NULL,
  params JSON NULL,                          -- {"energy": 12} — اسکالر
  override_duration_minutes SMALLINT NULL,
  UNIQUE KEY uniq_step (protocol_id, session_number),
  CONSTRAINT fk_step_protocol FOREIGN KEY (protocol_id) REFERENCES course_protocols(id) ON DELETE CASCADE
);
```

فرزند aggregate با ریشهٔ `CourseProtocol`.

## `treatment_courses`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `patient_record_id` | INT NOT NULL | FK ON DELETE RESTRICT |
| `service_item_id` | INT NOT NULL | FK ON DELETE RESTRICT |
| `protocol_id` | INT NOT NULL | FK ON DELETE RESTRICT |
| `session_count` | SMALLINT NOT NULL | **snapshot** |
| `min_days` | SMALLINT NOT NULL | **snapshot** |
| `ideal_days` | SMALLINT NOT NULL | **snapshot** |
| `max_days` | SMALLINT NOT NULL | **snapshot** |
| `patient_package_id` | INT NULL | FK → `patient_packages.id` ON DELETE SET NULL |
| `preferred_resource_id` | INT NULL | FK → `clinic_resources.id` ON DELETE SET NULL |
| `status` | VARCHAR(12) NOT NULL DEFAULT 'active' | `active`\|`completed`\|`abandoned` |
| `abandon_reason` | VARCHAR(255) NULL | |
| `started_at` | INT NOT NULL | |
| `completed_at` | INT NULL | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_courses_tenant  (entity_type, entity_id, status, started_at)
KEY idx_courses_patient (patient_record_id, status)
UNIQUE KEY uniq_active_course (patient_record_id, service_item_id, status)
```

⚠️ `uniq_active_course` با MariaDB روی مقدار `status` کار نمی‌کند به شکلی که فقط
`active` را یکتا کند (چند ردیف `completed` مجازند). راه درست: **قید اپلیکیشنی** در
`CourseStarter` + کلید یکتای جزئی که MariaDB ندارد.

جایگزین: یک ستون `active_course_key VARCHAR(64) NULL UNIQUE` با همان الگوی
`Appointment.active_slot_key`:

```php
$this->activeCourseKey = $this->status === self::STATUS_ACTIVE
    ? sprintf('%d:%d', $this->patient->getId(), $this->service->getId())
    : null;
```

الگوی اثبات‌شدهٔ همین کدبیس — استفاده‌اش کن، دوباره اختراع نکن.

## `course_sessions`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `course_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `session_number` | SMALLINT NOT NULL | |
| `params` | JSON NULL | **snapshot** از `course_protocol_steps` |
| `appointment_id` | INT NULL UNIQUE | FK ON DELETE SET NULL |
| `status` | VARCHAR(12) NOT NULL DEFAULT 'planned' | `planned`\|`booked`\|`completed`\|`skipped` |
| `completed_at` | INT NULL | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
UNIQUE KEY uniq_course_session (course_id, session_number)
UNIQUE KEY uniq_session_appointment (appointment_id)
KEY idx_sessions_tenant (entity_type, entity_id, status)
KEY idx_sessions_course (course_id, session_number)
```

`uniq_session_appointment`: یک نوبت به بیش از یک جلسهٔ دوره وصل نمی‌شود.

## تغییر `appointments`

```sql
ALTER TABLE appointments
  ADD COLUMN course_session_id INT NULL,
  ADD CONSTRAINT fk_appointments_course_session
      FOREIGN KEY (course_session_id) REFERENCES course_sessions(id) ON DELETE SET NULL,
  ADD KEY idx_appointments_course_session (course_session_id);
```

دو طرفه است (`course_sessions.appointment_id` هم وجود دارد) — عمدی: لیست نوبت‌های پنل
باید بدون JOIN بفهمد نوبت جزو دوره است، و صفحهٔ دوره باید بدون JOIN نوبت را پیدا کند.
هر دو در `CourseSessionLinker` **هم‌زمان** ست می‌شوند؛ هیچ جای دیگری ننویسد.

## Migration

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

بدون backfill.

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

| جدول | وضعیت |
|---|---|
| `course_protocols`, `treatment_courses`, `course_sessions` | جفت tenant |
| `course_protocol_steps` | `AGGREGATE_CHILDREN` → ریشه `CourseProtocol` |
