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

## `cancellation_policies`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | |
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
| `service_item_id` | INT NULL | NULL = پیش‌فرض محیط — FK ON DELETE CASCADE |
| `free_window_hours` | SMALLINT NOT NULL DEFAULT 24 | |
| `penalty_mode` | VARCHAR(10) NOT NULL DEFAULT 'none' | `none`\|`percent`\|`fixed` |
| `penalty_value` | INT NOT NULL DEFAULT 0 | درصد ۰..۱۰۰ یا ریال |
| `deposit_refundable` | TINYINT(1) NOT NULL DEFAULT 0 | پس از پنجرهٔ رایگان |
| `credit_refundable` | TINYINT(1) NOT NULL DEFAULT 1 | اعتبار پکیج |
| `no_show_threshold` | SMALLINT NOT NULL DEFAULT 3 | |
| `risk_tag_uuid` | VARCHAR(36) NULL | ارجاع به `tenant_tags.uuid` — بدون FK، الگوی موجود پروژه |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
UNIQUE KEY uniq_cancel_policy_scope (entity_type, entity_id, service_item_id)
KEY idx_cancel_policies_tenant (entity_type, entity_id, active)
```

`risk_tag_uuid` بدون FK — همان الگوی `DiscountRule.target_tag_uuid` موجود.

## `no_show_records`

```sql
CREATE TABLE no_show_records (
  id INT PRIMARY KEY AUTO_INCREMENT,
  uuid VARCHAR(36) NOT NULL UNIQUE,
  entity_type VARCHAR(10) NOT NULL,
  entity_id INT NOT NULL,
  patient_record_id INT NOT NULL,
  appointment_id INT NOT NULL,
  recorded_at INT NOT NULL,
  recorded_by INT NULL,
  UNIQUE KEY uniq_no_show_appointment (appointment_id),      -- یک بار per نوبت
  KEY idx_no_show_patient (patient_record_id, recorded_at),  -- کوئری شمارش ۱۲ ماه
  KEY idx_no_show_tenant  (entity_type, entity_id, recorded_at),
  CONSTRAINT fk_ns_patient FOREIGN KEY (patient_record_id) REFERENCES patient_records(id) ON DELETE CASCADE,
  CONSTRAINT fk_ns_appt    FOREIGN KEY (appointment_id)    REFERENCES appointments(id)    ON DELETE CASCADE
);
```

جدول جدا و نه یک ستون شمارنده روی بیمار — همان استدلال دفتر اعتبار تسک ۱۱:
شمارنده، «چه زمانی و کدام نوبت» را از دست می‌دهد و پنجرهٔ ۱۲ ماهه غیرقابل محاسبه می‌شود.

`uniq_no_show_appointment`: تغییر وضعیت به `no_show` ممکن است دوبار اتفاق بیفتد
(idempotency)؛ رکورد دوم ثبت نشود.

## `waitlist_entries`

| ستون | نوع | توضیح |
|---|---|---|
| `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 CASCADE |
| `service_item_id` | INT NOT NULL | FK ON DELETE CASCADE |
| `branch_id` | INT NULL | FK ON DELETE CASCADE |
| `desired_from` | INT NOT NULL | |
| `desired_to` | INT NOT NULL | |
| `preferred_day_parts` | JSON NULL | `["morning","evening"]` |
| `priority` | SMALLINT NOT NULL DEFAULT 0 | |
| `status` | VARCHAR(12) NOT NULL DEFAULT 'waiting' | `waiting`\|`notified`\|`converted`\|`expired` |
| `notified_at` | INT NULL | آخرین اطلاع |
| `notify_count` | SMALLINT NOT NULL DEFAULT 0 | سقف برای جلوگیری از اسپم |
| `converted_appointment_id` | INT NULL | FK ON DELETE SET NULL |
| `created_at`/`updated_at` | INT NOT NULL | |

```sql
KEY idx_waitlist_match  (service_item_id, branch_id, status, desired_from, desired_to)
KEY idx_waitlist_tenant (entity_type, entity_id, status, created_at)
KEY idx_waitlist_patient (patient_record_id, status)
```

`idx_waitlist_match` کوئری داغ است: «چه کسانی منتظر این سرویس در این بازه‌اند؟»

```sql
SELECT * FROM waitlist_entries
WHERE service_item_id = ? AND (branch_id = ? OR branch_id IS NULL)
  AND status = 'waiting'
  AND desired_from <= :freedEnd AND desired_to >= :freedStart
ORDER BY priority DESC, created_at ASC
LIMIT 10
```

`preferred_day_parts` در PHP فیلتر می‌شود (JSON قابل ایندکس مطمئن نیست و نتیجه ≤ ۱۰ ردیف است).

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

وضعیت‌های `cancelled_by_user`, `cancelled_by_doctor`, `no_show` از قبل هستند.
`deposit_amount_rials` هم.

## جریمه در دفتر مالی موجود

جدول جدید ندارد. `WalletTransaction` موجود استفاده می‌شود:

```php
new WalletTransaction(
    user: $appt->getUser(),
    amountRials: -$penalty,
    kind: 'cancellation_penalty',      // ← مقدار جدید در enum موجود
    reference: $appt->getUuid(),
);
$tx->setRecordedEntity($appt->getEntityType(), $appt->getEntityId());   // per-محیط، طبق tenancy.md
```

`setRecordedEntity` اجباری است، وگرنه جریمه در دفتر همهٔ محیط‌ها دیده می‌شود
(`docs/architecture/tenancy.md`، بخش کیف پول).

## 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:cancellation:seed-default-policy --force
```

`seed-default-policy` برای هر محیط یک سیاست پیش‌فرض محافظه‌کار می‌سازد:
`free_window_hours=24, penalty_mode=none, deposit_refundable=true, credit_refundable=true`.

**پیش‌فرض بدون جریمه** عمدی است: فعال شدن ناگهانی جریمه روی بیماران موجود، شکایت است.
کلینیک خودش باید فعالش کند.

## پاکسازی

```bash
ddev exec php bin/console app:waitlist:expire   # روزانه
```

ورودی‌هایی که `desired_to` گذشته → `status='expired'`.

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

| جدول | وضعیت |
|---|---|
| `cancellation_policies`, `no_show_records`, `waitlist_entries` | جفت tenant |
