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

## `price_lists`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `branch_id` | INT NULL | NULL = همهٔ شعب محیط |
| `name` | VARCHAR(150) NOT NULL | «نیمهٔ دوم ۱۴۰۵» |
| `valid_from` | INT NOT NULL | نیمه‌شب روز شروع |
| `valid_to` | INT NULL | NULL = بی‌پایان |
| `status` | VARCHAR(10) NOT NULL DEFAULT 'draft' | `draft`\|`active`\|`archived` |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_price_lists_tenant (entity_type, entity_id, status, valid_from)
KEY idx_price_lists_branch (branch_id, status, valid_from)
```

تداخل بازه در سطح اپلیکیشن بررسی می‌شود (`activate`)، نه DB — MariaDB محدودیت بازه‌ای ندارد
و راه سطل زمانی تسک ۰۷ اینجا بی‌مورد است چون تعداد لیست‌ها کم و تغییرشان نادر است.

## `price_list_items`

```sql
CREATE TABLE price_list_items (
  id INT PRIMARY KEY AUTO_INCREMENT,
  price_list_id INT NOT NULL,
  service_item_id INT NULL,        -- قیمت سرویس
  service_option_id INT NULL,      -- قیمت آیتم
  price_rials INT NOT NULL,
  UNIQUE KEY uniq_pli_service (price_list_id, service_item_id),
  UNIQUE KEY uniq_pli_option  (price_list_id, service_option_id),
  KEY idx_pli_list (price_list_id),
  CONSTRAINT fk_pli_list    FOREIGN KEY (price_list_id)     REFERENCES price_lists(id)     ON DELETE CASCADE,
  CONSTRAINT fk_pli_service FOREIGN KEY (service_item_id)   REFERENCES service_items(id)   ON DELETE CASCADE,
  CONSTRAINT fk_pli_option  FOREIGN KEY (service_option_id) REFERENCES service_options(id) ON DELETE CASCADE
);
```

دقیقاً یکی از `service_item_id` / `service_option_id` غیر-NULL (قید اپلیکیشنی).
فرزند aggregate با ریشهٔ `PriceList`.

## `price_snapshots`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `appointment_id` | INT NOT NULL UNIQUE | FK ON DELETE CASCADE — یک snapshot per نوبت |
| `base_rials` | INT NOT NULL | |
| `options_rials` | INT NOT NULL DEFAULT 0 | |
| `discount_rials` | INT NOT NULL DEFAULT 0 | |
| `insurance_base_rials` | INT NOT NULL DEFAULT 0 | |
| `insurance_supplementary_rials` | INT NOT NULL DEFAULT 0 | |
| `tax_rials` | INT NOT NULL DEFAULT 0 | |
| `final_rials` | INT NOT NULL | |
| `deposit_rials` | INT NOT NULL DEFAULT 0 | |
| `applied_policy_ids` | JSON NULL | `[{id, version}]` |
| `price_list_id` | INT NULL | FK SET NULL — کدام لیست مبنا بود |
| `created_at` | INT NOT NULL | |

```sql
UNIQUE KEY uniq_snapshot_appointment (appointment_id)
KEY idx_snapshot_tenant (entity_type, entity_id, created_at)
```

`UNIQUE` روی `appointment_id`: یک نوبت یک فاکتور رزرو دارد. `reschedule` نوبت **جدید**
می‌سازد (تسک ۰۷) پس snapshot جدید هم می‌گیرد و قدیمی سالم می‌ماند.

⚠️ همهٔ مبالغ `INT` ریال. `DECIMAL` یا `FLOAT` ننویس — بقیهٔ پروژه (`price_rials`,
`visit_price_rials`, `deposit_amount_rials`) همه `INT` ریال‌اند و قاطی کردن دو نوع یعنی
خطای گردکردن در جمع فاکتور.

## `price_snapshot_lines`

```sql
CREATE TABLE price_snapshot_lines (
  id INT PRIMARY KEY AUTO_INCREMENT,
  snapshot_id INT NOT NULL,
  kind VARCHAR(15) NOT NULL,          -- base|option|discount|insurance|tax|package
  label VARCHAR(200) NOT NULL,        -- snapshot متنی — نام لحظهٔ ثبت
  amount_rials INT NOT NULL,          -- منفی برای تخفیف و سهم بیمه
  source_type VARCHAR(20) NULL,       -- service|option|discount_rule|policy|insurance
  source_id INT NULL,                 -- بدون FK — منبع ممکن است حذف شود
  sort_order SMALLINT NOT NULL DEFAULT 0,
  KEY idx_psl_snapshot (snapshot_id, sort_order),
  CONSTRAINT fk_psl_snapshot FOREIGN KEY (snapshot_id) REFERENCES price_snapshots(id) ON DELETE CASCADE
);
```

`source_id` **بدون FK** عمدی: قانون تخفیف ممکن است فردا حذف شود ولی فاکتور دیروز باید
همان‌طور بماند. `label` هم به همین دلیل کپی متنی است، نه JOIN.

## `deposit_policies`

```sql
CREATE TABLE deposit_policies (
  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 NULL,            -- NULL = پیش‌فرض محیط
  mode VARCHAR(10) NOT NULL,           -- none|fixed|percent
  value INT NOT NULL DEFAULT 0,
  min_rials INT NULL,
  max_rials INT NULL,
  active TINYINT(1) NOT NULL DEFAULT 1,
  created_at INT NOT NULL,
  updated_at INT NOT NULL,
  UNIQUE KEY uniq_deposit_scope (entity_type, entity_id, service_item_id),
  KEY idx_deposit_tenant (entity_type, entity_id, active)
);
```

## تغییر جدول موجود

هیچ. `appointments.visit_price_rials`, `deposit_required`, `deposit_amount_rials`,
`insurance_base_id`, `insurance_supplementary_id` همه استفاده می‌شوند و کافی‌اند.

`Tariff` هم دست‌نخورده می‌ماند و در زنجیرهٔ `PriceResolver` سطر ۴ است.

## Migration

```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console app:pricing:backfill-snapshots --force
```

`app:pricing:backfill-snapshots` برای نوبت‌های `confirmed` آینده که snapshot ندارند، یکی
از `visit_price_rials` موجود می‌سازد (یک ردیف `base`). بدون آن، صفحهٔ فاکتور برای
نوبت‌های موجود خالی است.

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

| جدول | وضعیت |
|---|---|
| `price_lists`, `price_snapshots`, `deposit_policies` | جفت tenant |
| `price_list_items`, `price_snapshot_lines` | `AGGREGATE_CHILDREN` |
