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

## تغییر جدول موجود: `service_items`

```sql
ALTER TABLE service_items
  ADD COLUMN service_category_id INT NULL AFTER section_id,
  ADD COLUMN session_count SMALLINT NOT NULL DEFAULT 1,      -- ۱ = تک‌جلسه، >۱ = دوره‌ای
  ADD COLUMN preparation_note TEXT NULL,                     -- «آماده‌سازی بیمار» — مستند بند ۵
  ADD CONSTRAINT fk_service_items_category
      FOREIGN KEY (service_category_id) REFERENCES service_categories(id) ON DELETE SET NULL,
  ADD KEY idx_service_items_category (service_category_id);
```

`duration_minutes` موجود **حذف نمی‌شود** و همچنان «مدت پایهٔ سرویس» است. آیتم‌ها روی آن
اضافه می‌کنند. در حالت `booking_mode=service` فعلی هیچ تغییری در رفتار نیست چون هیچ
سرویسی گروه ندارد و `totalMinutes` برابر همان `duration_minutes` می‌ماند.

## `service_categories` — درختی

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `parent_id` | INT NULL | FK → خودش، ON DELETE RESTRICT |
| `name` | VARCHAR(150) NOT NULL | |
| `path` | VARCHAR(255) NOT NULL | materialized path: `/1/7/23/` |
| `depth` | TINYINT NOT NULL DEFAULT 0 | |
| `sort_order` | SMALLINT NOT NULL DEFAULT 0 | |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_svc_cat_tenant (entity_type, entity_id, active)
KEY idx_svc_cat_parent (parent_id, sort_order)
KEY idx_svc_cat_path   (path)
```

**materialized path** به‌جای adjacency خالص: تسک ۰۹ شرط «سرویس در دستهٔ جراحی یا
زیردسته‌هایش» را می‌خواهد و با `path LIKE '/1/7/%'` یک کوئری است، نه یک پیمایش بازگشتی.

سقف عمق: ۴. در `ServiceCategoryService` اجبار شود.

## `item_groups`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | از سرویس مشتق می‌شود |
| `service_item_id` | INT NOT NULL | FK → `service_items.id` ON DELETE CASCADE |
| `name` | VARCHAR(150) NOT NULL | «نواحی موردنظر» |
| `min_select` | SMALLINT NOT NULL DEFAULT 0 | ۰ = اختیاری |
| `max_select` | SMALLINT NULL | NULL = نامحدود |
| `sort_order` | SMALLINT NOT NULL DEFAULT 0 | |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_item_groups_tenant  (entity_type, entity_id, active)
KEY idx_item_groups_service (service_item_id, sort_order)
```

## `service_options` — «آیتم» مستند

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | از request می‌آید |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `item_group_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `name` | VARCHAR(150) NOT NULL | |
| `solo_minutes` | SMALLINT NOT NULL | «زمان تنها» |
| `additional_minutes` | SMALLINT NULL | «زمان اضافه»؛ NULL → `solo_minutes` |
| `price_rials` | INT NOT NULL DEFAULT 0 | |
| `sort_order` | SMALLINT NOT NULL DEFAULT 0 | |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_service_options_tenant (entity_type, entity_id, active)
KEY idx_service_options_group  (item_group_id, sort_order)
```

قید اپلیکیشنی: `additional_minutes <= solo_minutes` (زمان اضافه هرگز بیشتر از زمان تنها
نیست — آماده‌سازی که دو بار نمی‌شود). نقض → `422` با پیام روشن.

## `service_option_relations`

```sql
CREATE TABLE service_option_relations (
  id INT PRIMARY KEY AUTO_INCREMENT,
  source_option_id INT NOT NULL,
  target_option_id INT NOT NULL,
  type VARCHAR(20) NOT NULL,                  -- incompatible | requires
  UNIQUE KEY uniq_sor (source_option_id, target_option_id, type),
  KEY idx_sor_target (target_option_id, type),
  CONSTRAINT fk_sor_source FOREIGN KEY (source_option_id) REFERENCES service_options(id) ON DELETE CASCADE,
  CONSTRAINT fk_sor_target FOREIGN KEY (target_option_id) REFERENCES service_options(id) ON DELETE CASCADE
);
```

فرزند aggregate با ریشهٔ `ServiceOption`. قید اپلیکیشنی: `source != target`.

## `service_branch_overrides`

```sql
CREATE TABLE service_branch_overrides (
  id INT PRIMARY KEY AUTO_INCREMENT,
  uuid VARCHAR(36) NOT NULL UNIQUE,
  entity_type VARCHAR(10) NOT NULL,
  entity_id INT NOT NULL,
  service_item_id INT NOT NULL,
  branch_id INT NOT NULL,
  price_rials INT NULL,                       -- NULL = ارث از سرویس
  duration_minutes SMALLINT NULL,             -- NULL = ارث از سرویس
  bookable TINYINT(1) NULL,                   -- NULL = ارث؛ 0 = این شعبه ارائه نمی‌دهد
  created_at INT NOT NULL,
  updated_at INT NOT NULL,
  UNIQUE KEY uniq_sbo (service_item_id, branch_id),
  KEY idx_sbo_tenant (entity_type, entity_id),
  KEY idx_sbo_branch (branch_id),
  CONSTRAINT fk_sbo_service FOREIGN KEY (service_item_id) REFERENCES service_items(id) ON DELETE CASCADE,
  CONSTRAINT fk_sbo_branch  FOREIGN KEY (branch_id)       REFERENCES branches(id)      ON DELETE CASCADE
);
```

سه ستون تهی‌پذیرند تا override جزئی ممکن باشد: فقط قیمت، بدون دست زدن به مدت.

## Migration و backfill

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

backfill لازم نیست: سرویس‌های موجود گروه ندارند، `DurationCalculator` مقدار
`service_items.duration_minutes` را برمی‌گرداند و رفتار حالت `service` بدون تغییر می‌ماند.

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

| جدول | وضعیت |
|---|---|
| `service_categories`, `item_groups`, `service_options`, `service_branch_overrides` | جفت tenant |
| `service_option_relations` | `AGGREGATE_CHILDREN` → ریشه `ServiceOption` |

`TenantLookupInventoryTest` شمارنده دارد؛ `findByUuid` های جدید (`ServiceOptionRepository`,
`ItemGroupRepository`) باید با `TenantOwnershipChecker` جفت شوند و بعد عدد به‌روز شود.
