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

## `policies`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `branch_id` | INT NULL | NULL = همهٔ شعب — FK ON DELETE CASCADE |
| `name` | VARCHAR(200) NOT NULL | «خدمات جراحی به جراح نیاز دارند» |
| `category` | VARCHAR(20) NOT NULL | یکی از شش دسته |
| `priority` | SMALLINT NOT NULL DEFAULT 0 | |
| `specificity` | SMALLINT NOT NULL DEFAULT 0 | محاسبه‌شده هنگام ذخیره |
| `version` | SMALLINT NOT NULL DEFAULT 1 | |
| `valid_from` | INT NULL | |
| `valid_to` | INT NULL | |
| `active` | TINYINT(1) NOT NULL DEFAULT 0 | **پیش‌فرض غیرفعال** |
| `conditions` | JSON NOT NULL | |
| `effects` | JSON NOT NULL | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_policies_lookup (entity_type, entity_id, category, active, valid_from)
KEY idx_policies_branch (branch_id, category, active)
```

`idx_policies_lookup` کوئری داغ است: «قوانین فعال دستهٔ X این محیط که امروز معتبرند».
با ۶ دسته و معمولاً < ۵۰ قانون per محیط، این کوئری همیشه ارزان است — و باید بماند.
اگر روزی قوانین به هزاران رسیدند، کش per (محیط، دسته) اضافه شود، نه ایندکس پیچیده‌تر.

## `policy_version_log`

```sql
CREATE TABLE policy_version_log (
  id INT PRIMARY KEY AUTO_INCREMENT,
  policy_id INT NOT NULL,
  version SMALLINT NOT NULL,
  snapshot JSON NOT NULL,             -- کل قانون در آن نسخه، نه diff
  valid_from INT NULL,
  valid_to INT NULL,
  changed_by INT NULL,                -- FK users ON DELETE SET NULL
  changed_at INT NOT NULL,
  UNIQUE KEY uniq_policy_version (policy_id, version),
  KEY idx_pvl_policy (policy_id, version),
  CONSTRAINT fk_pvl_policy FOREIGN KEY (policy_id) REFERENCES policies(id) ON DELETE CASCADE
);
```

`snapshot` کامل است، نه diff: بازسازی نسخهٔ ۳ از یک قانون که الان نسخهٔ ۹ است، باید یک
`SELECT` باشد. حجم ناچیز (JSON چند کیلوبایتی × چند ده نسخه).

## هیچ تغییری در `discount_rules`

جدول و entity موجود دست‌نخورده می‌مانند. `PricingPolicyEngine` **هر دو** را می‌خواند:

```php
$effects = array_merge(
    $this->discountEngine->evaluate($ctx),        // DiscountRule موجود
    $this->pricingPolicies->evaluate($ctx),       // Policy دستهٔ pricing
);
$combined = $this->combiner->combine($effects);   // ترکیب واحد
```

دلیل عدم مهاجرت در implementation_notes بند ۱.

## `applied_policy_ids` — تسک ۰۸

ستون از قبل در `price_snapshots` هست. قرارداد مقدارش اینجا تعیین می‌شود:

```json
[
  { "source": "policy",        "id": 12, "version": 3, "name": "تخفیف VIP" },
  { "source": "discount_rule", "id": 5,  "version": 1, "name": "تخفیف تولد" }
]
```

`name` کپی متنی — همان دلیل `price_snapshot_lines.label`: قانون ممکن است حذف شود.

## ذخیرهٔ قوانین اعمال‌شده روی خودِ نوبت

قوانین غیرقیمتی هم باید ثبت شوند (مستند: «هر نوبت فهرست قانون‌هایی که رویش اعمال شده را
ذخیره می‌کند»)، ولی `price_snapshots` جای قوانین منبع و زمان نیست.

```sql
ALTER TABLE appointments
  ADD COLUMN applied_policies JSON NULL;
```

همان قرارداد بالا. یک ستون JSON کافی است — هیچ کوئری‌ای روی آن زده نمی‌شود، فقط برای
آدیت و پاسخ به «چرا این نوبت ۷۵ دقیقه شد؟» خوانده می‌شود.

## Migration

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

بدون backfill. هیچ قانونی از قبل وجود ندارد و `DiscountRule` ها سر جایشان می‌مانند.

## نمونهٔ داده برای تست

```bash
ddev exec php bin/console app:policy:seed-examples --tenant=clinic:12 --force
```

پنج قانون نمونه (یکی از هر دسته جز `selection`)، همه با `active=false` تا کلینیک اول
آزمایششان کند:

```
resource    | خدمات جراحی به جراح نیاز دارند
timing      | حداقل مدت درمان پیچیده یک ساعت است
spacing     | حداقل ۲۱ روز از جلسهٔ قبلی لیزر
eligibility | بیمار زیر ۱۸ سال بدون رضایت والدین نمی‌شود
pricing     | بیمار VIP ده درصد تخفیف
```

این نمونه‌ها هم مستندات زنده‌اند و هم داده تست تسک ۱۰.

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

| جدول | وضعیت |
|---|---|
| `policies` | جفت tenant |
| `policy_version_log` | `AGGREGATE_CHILDREN` → ریشه `Policy` |
