# تسک ۰۹ — موتور قوانین شش‌دسته‌ای

**فاز:** ۲ (قوانین) · **وابستگی:** ۰۵، ۰۶، ۰۸ · **زمان:** ۲۰-۲۴ ساعت

---

## هدف

مستند بند ۸: شش دستهٔ قانون، هر کدام با نقطهٔ اجرای مشخص، شرط از فهرست بسته،
اولویت‌دار، نسخه‌بندی‌شده، و **بدون کد دلخواه**.

| دسته | کِی اجرا می‌شود | نقطهٔ اتصال در کد |
|---|---|---|
| انتخاب (`selection`) | موقع انتخاب آیتم | `ServiceSelectionValidator` (تسک ۰۴) |
| صلاحیت بیمار (`eligibility`) | قبل از جستجوی وقت | `AvailabilityEngine::search` ابتدا · `BookingService::confirm` مرحلهٔ ۳ |
| منبع (`resource`) | موقع ساخت برنامه | `AppointmentPlanBuilder` مرحلهٔ ۷ (تسک ۰۵) |
| زمان (`timing`) | موقع ساخت برنامه | همان |
| فاصلهٔ زمانی (`spacing`) | موقع جستجوی وقت | `AvailabilityEngine` مرحلهٔ ۶ |
| قیمت (`pricing`) | بعد از نهایی شدن برنامه | `PricingEngine` مرحلهٔ ۳ (تسک ۰۸) |

همهٔ این قلاب‌ها در تسک‌های ۰۴ تا ۰۸ از قبل به‌صورت no-op گذاشته شده‌اند. این تسک
پُرشان می‌کند.

## وضعیت فعلی

`DiscountRule` + `DiscountEngine` تنها موتور قانون موجود است و **الگوی درستی** دارد:

```php
// src/Discount/Entity/DiscountRule.php
public const TYPES = [TYPE_PATIENT_TAG, TYPE_INVOICE_AMOUNT, TYPE_SPECIFIC_PATIENT,
                      TYPE_OCCASION, TYPE_SERVICE, TYPE_VISIT_COUNT];   // enum بسته ✅
private int  $priority;        // ✅
private bool $combinable;      // ✅
private ?int $validFrom;       // ✅
private ?int $validTo;         // ✅
```

فقط دستهٔ «قیمت» را پوشش می‌دهد، نسخه‌بندی ندارد، و محیط آزمایش ندارد.

## دامنه

**هست:**
- `Policy` — قانون با دسته، شرط، نتیجه، اولویت، بازهٔ اعتبار، نسخه
- `PolicyVersionLog` — تاریخچهٔ نسخه‌ها (قانون ویرایش نمی‌شود، نسخه می‌گیرد)
- `ConditionEvaluator` — ارزیابی شرط از فهرست بسته
- شش موتور دسته، هر کدام یک کلاس
- حل تناقض: اولویت → اختصاصی‌بودن → قدمت
- ترکیب اثرها طبق جدول مستند بند ۸
- ثبت قانون‌های اعمال‌شده روی نوبت (`applied_policy_ids` تسک ۰۸)

**نیست:** فرم ساخت قانون و محیط آزمایش (تسک ۱۰ — همان تسک UI است).
`DiscountRule` موجود **مهاجرت نمی‌کند**؛ کنار `Policy` دستهٔ `pricing` زندگی می‌کند
(دلیل در implementation_notes).

## Endpoint ها

| متد | مسیر | توضیح |
|---|---|---|
| GET | `/api/v1/policies` | لیست با فیلتر دسته/وضعیت |
| POST | `/api/v1/policy` | ساخت (نسخهٔ ۱) |
| GET | `/api/v1/policy/{uuid}` | جزئیات + تاریخچهٔ نسخه |
| POST | `/api/v1/policy/{uuid}/version` | نسخهٔ جدید با تاریخ شروع |
| POST | `/api/v1/policy/{uuid}/activate` \| `/deactivate` | |
| GET | `/api/v1/policy-schema` | فهرست بستهٔ فیلدها، عملگرها و اثرها per دسته |

`GET /policy-schema` برای تسک ۱۰ حیاتی است: فرم ساخت قانون از همین schema ساخته می‌شود،
نه از کد hard-coded در فرانت.

## معیار پذیرش

- ✅ موفق: قانون «خدمات دسته جراحی به جراح نیاز دارند» (دسته `resource`) →
  `POST /appointment-plan/preview` برای سرویسی در آن دسته، یک نیازمندی اضافه دارد.
- ✅ موفق: قانون «حداقل ۷ روز از جلسهٔ قبلی» (دسته `spacing`) → جستجوی وقت برای بیماری که
  ۳ روز پیش جلسه داشته، روزهای زودتر از روز هفتم را برنمی‌گرداند.
- ✅ موفق: قانون «بیمار زیر ۱۸ سال بدون رضایت والدین نمی‌شود» (دسته `eligibility`) →
  `confirm` با `422` و پیام قابل فهم رد می‌شود.
- ✅ موفق: قانون «بیمار VIP ۱۰٪ تخفیف» (دسته `pricing`) → در `price_snapshot_lines` یک
  ردیف `discount` با نام قانون ظاهر می‌شود و شناسه+نسخه در `applied_policy_ids` ثبت می‌شود.
- ✅ موفق (**قانون پنجم مستند**): بعد از ثبت نوبت، قانون نسخهٔ ۲ می‌گیرد →
  نوبت قبلی همان نسخهٔ ۱ را در `applied_policy_ids` دارد و فاکتورش تغییر نمی‌کند.
- ✅ موفق: دو قانون «حداقل مدت» با مقادیر ۴۵ و ۶۰ دقیقه → **بیشترین** برنده است (۶۰).
- ✅ موفق: دو قانون «اضافه کردن زمان» ۱۰ و ۵ دقیقه → **جمع** می‌شوند (۱۵).
- ✅ موفق: یک قانون «ممنوعیت» → کل عملیات رد می‌شود، حتی اگر ده قانون مجازکننده باشند.
- ❌ خطا: شرط با فیلد خارج از فهرست بسته → `422` با نام فیلد مجاز.
- ❌ خطا: نتیجه با نوع اثر ناسازگار با دسته → `422`.
- ⚠️ مرزی: دو قانون با اولویت مساوی → اختصاصی‌تر (شعبه بر محیط، سرویس بر دسته) برنده.
- ⚠️ مرزی: باز هم مساوی → قانون **قدیمی‌تر** برنده (مستند بند ۸).
- ⚠️ مرزی: قانون با `valid_from` آینده → در محاسبهٔ امروز اعمال نمی‌شود.
- ⚠️ مرزی: هیچ قانونی وجود ندارد → همه‌چیز مثل قبل کار می‌کند (تست سازگاری).
- ⚠️ مرزی: قانون `spacing` باید **قابل تبدیل به کوئری** باشد؛ ارزیابی per-slot در PHP
  برای ۹۰ روز غیرقابل قبول است (مستند بند ۸ صریح).

## خروجی

- `src/Policy/`
- `docs/api/policy.md` + سند معماری `docs/architecture/policy-engine.md`
- پر کردن همهٔ قلاب‌های no-op تسک‌های ۰۴ تا ۰۸
