# چک‌لیست — تسک ۰۹ (موتور قوانین شش‌دسته‌ای) > # ⛔ این تسک از محصول حذف شد > > **تصمیم مالک محصول، ۱۴۰۵/۰۵/۱۰:** مدل نوبت‌دهی به منبع/سرویس/گزینه محدود شد و هر چیز > خارج از آن حذف شد. کد، جدول‌ها، endpointها، تست‌ها و صفحات پنل این تسک در کامیت > «Remove the policy, package, course, cancellation and event subsystems» برداشته شدند. > > ریسکش پیش از اجرا دو بار مطرح و دو بار تأیید شد. ردیف‌های زیر **تاریخچه**اند، نه کار > جاری؛ برای برگرداندن به همان کامیت رجوع کنید. > > مدل جایگزین: [`docs/architecture/resource-first-model.md`](../../../architecture/resource-first-model.md) > و چک‌لیست [تسک ۱۵](../task-15-resource-first-model/checklist.md). **وضعیت کلی:** ✅ تمام‌شده با انحراف‌های ثبت‌شده · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۹ قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) · [red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md) > **انحراف اصلی از متن تسک:** به‌جای شش موتور جدا، یک `PolicyResolver` مشترک ساخته شد و > هر نقطهٔ مصرف حقایق خودش را می‌سازد. دلیل و بقیهٔ انحراف‌ها در > [docs/architecture/policy-engine.md](../../../architecture/policy-engine.md#تصمیمهای-ثبتشده-و-انحرافها). --- ## ۰. خط سرخ | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | ۳ تست، ۸ assertion | | ۰.۲ | **هیچ قانونی روی حالت `slot` اعمال نمی‌شود** | ✅ | ⭐ نقاط اجرا فقط `plan`/`hold`/`quote`/`selection`اند؛ مسیر اسلاتی هیچ‌کدام را صدا نمی‌زند | | ۰.۳ | `DiscountRule` مهاجرت نکرد و دست‌نخورده ماند | ✅ | قاعدهٔ انتخاب در `policy-engine.md` | | ۰.۴ | `NoPolicyRegressionTest`: بدون هیچ قانون، خروجی‌ها مثل تسک ۰۸ | ✅ | ⭐ `tests/Policy/NoPolicyRegressionTest.php` | | ۰.۵ | کد دلخواه در قانون **ممنوع** — فقط فهرست بسته | ✅ | `PolicySchema::FIELDS/OPERATORS/EFFECTS` | | ۰.۶ | تودرتویی شرط ممنوع — فقط `all`/`any` یک‌سطحی | ✅ | کلید ناشناس در ریشهٔ شرط هم ۴۲۲ می‌گیرد | ## ۱. بک‌اند | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۱.۱ | `Policy` · `PolicyVersionLog` | ✅ | `UNIQUE(policy_id, version)` | | ۱.۲ | `active = false` پیش‌فرض | ✅ | تست `testANewPolicyIsADraftUntilActivated` | | ۱.۳ | `FieldRegistry` — schema/extract/assert | ✅ | ساخته شد و همان بدو ورود اولین موردش را گرفت: `last_visit_at` تبلیغ می‌شد و هیچ‌جا پر نمی‌شد. یادداشت قبلی: به‌جای رجیستری، `PolicySchema` (فهرست) + `ConditionEvaluator` (assert) + حقایقی که هر نقطه می‌سازد. **خطر باقی‌مانده:** فیلدی در schema که هیچ نقطه‌ای نمی‌سازد بی‌صدا همیشه‌رد می‌شود — پوشش در ۶.۷ | | ۱.۴ | `OperatorRegistry` با یازده عملگر شامل `days_since` | ✅ | هر یازده تا، با تست واحد per عملگر؛ `between` بازهٔ بسته و `days_since` روی `last_visit_at` | | ۱.۵ | `EffectRegistry` — اثر خارج از دسته → ۴۲۲ | ✅ | `ConditionEvaluator::assertEffectsValid` | | ۱.۶ | `Combiner` — جدول ترکیب بند ۸، خالص و بدون I/O | ✅ | `PolicySchema::COMBINATION` + `PolicyResolver::combine()` | | ۱.۷ | `PolicyResolver` — اولویت → اختصاصی‌بودن → قدمت | ✅ | `comparator()` | | ۱.۸ | `specificity` هنگام **ذخیره** محاسبه می‌شود | ✅ | ستون `specificity` با وزن‌های مستند + مهاجرت backfill. یادداشت قبلی: هنگام اجرا (`Policy::specificity()`) — دلیل ثبت شد؛ در خروجی API هم برمی‌گردد | | ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ✅ | `PolicyEngine` + شش زیرکلاس؛ حل تناقض و ترکیب در `PolicyResolver` می‌ماند (شش کپی = شش جای شکستن) | | ۱.۱۰ | `evaluateIsolated()` روی هر شش موتور | ✅ | `PolicyResolver::evaluateOne()` در تسک ۱۰ اضافه شد — یک resolver، نه شش موتور | | ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری نه حلقه | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `spacing` در لحظهٔ رزرو موقت اجرا می‌شود (یک کوئری `MAX(slot_start)`)، نه در تولید کاندید | | ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ✅ | `AppointmentPlanBuilder::applyResourcePolicies` ممنوعیت را **پیش از** رسیدن به موتور دسترس‌پذیری می‌خواند | | ۱.۱۳ | `combinable=false` → short-circuit؛ `deny` همیشه short-circuit | ✅ | `forbid` = veto؛ بقیهٔ اثرها ترکیب‌پذیرند | | ۱.۱۴ | فیلد بی‌مقدار → `false` **با لاگ** | ✅ | ⭐ `warning` با نام قانون و فهرست حقایق موجود — قانونی که هر بار به این خط برسد عملاً خاموش است | | ۱.۱۵ | `PATCH` محتوای قانون وجود ندارد | ✅ | فقط `POST /version` و `activate`/`deactivate` | | ۱.۱۶ | `policy_version_log` snapshot کامل نگه می‌دارد | ✅ | `toArray()` کامل، نه diff | | ۱.۱۷ | `valid_from` گذشته در نسخهٔ جدید → ۴۲۲ | ✅ | روی `POST /version` رد می‌شود؛ روی نسخهٔ نخست آزاد است. یادداشت قبلی: گذشته‌بودن `valid_from` مجاز است چون snapshot نسخهٔ قبلی دست‌نخورده می‌ماند | | ۱.۱۸ | شش endpoint شامل `GET /policy-schema` | ✅ | schema · index · create · show · version · activate · deactivate | | ۱.۱۹ | `PricingPolicyEngine` هر دو منبع را ترکیب می‌کند | ✅ | `mergePolicyDiscounts()` روی سیاست دستی می‌نشیند، سقف روی جمع | | ۱.۲۰ | `TenantOwnershipChecker` روی هر uuid از request | ✅ | `requirePolicy`/`requireItem`/`requireCategory` — تست ۴۰۴ | ## ۲. پر کردن قلاب‌های تسک‌های قبل | # | قلاب | وضعیت | یادداشت | |---|---|---|---| | ۲.۱ | تسک ۰۴ — `ServiceSelectionValidator` | ✅ | `policyErrors()` | | ۲.۲ | تسک ۰۵ — `AppointmentPlanBuilder` | ✅ | `applyTimingPolicies()` + `applyResourcePolicies()`، روی سرویس بی‌الگو هم | | ۲.۳ | تسک ۰۶ — `AvailabilityEngine` → `Spacing` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — جایش `BookingPolicyGuard` شد (بند ۱.۱۱/۱.۱۲) | | ۲.۴ | تسک ۰۷ — `Eligibility` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — در `hold` نه `confirm` — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف می‌کند هم صندلی را | | ۲.۵ | تسک ۰۸ — `PricingEngine` | ✅ | | | ۲.۶ | **هیچ امضایی عوض نشد** | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (`PolicyResolver` / `BookingPolicyGuard`) — با DI خودکار بی‌اثر | ## ۳. دیتابیس | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۳.۱ | `policies` + `policy_version_logs` | ✅ | `Version20260731061814` | | ۳.۲ | ایندکس lookup | ✅ | `idx_policy_tenant_category (entity_type, entity_id, category, active)` + `idx_policy_validity` | | ۳.۳ | `appointments.applied_policies` | — | ردپا در `PriceSnapshot.sources.applied_policies` است؛ ستون دوم یعنی دو منبع حقیقت برای یک چیز | | ۳.۴ | قرارداد `{uuid, version, name}` | ✅ | `name` کپی متنی است نه ارجاع | | ۳.۵ | `policy_version_logs` در `AGGREGATE_CHILDREN` | ✅ | `GlobalTables` | | ۳.۶ | `app:policy:seed-examples` | — | `PolicyTemplateRegistry` تسک ۱۰ همان کار را از داخل UI می‌کند و کاربر نتیجه را پیش از ساخت می‌بیند | | ۳.۷ | `TenantSchemaCoverageTest` سبز | ✅ | | | ۳.۸ | ستون `condition` به `condition_json` تغییر کرد | ✅ | `condition` در MariaDB کلمهٔ کلیدی است و هر INSERT را می‌شکست؛ نام فیلد در API همان `condition` ماند | ## ۴. کارایی | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۴.۱ | `AvailabilityPerformanceTest` با قوانین فعال سبز | ✅ | ⭐⭐ سبز — و چون `spacing` وارد تولید کاندید نشد، تعداد کوئری اصلاً تغییر نکرد | | ۴.۲ | `spacing` تعداد کوئری ثابت دارد | ✅ | یک `MAX(slot_start)` به‌ازای هر رزرو، مستقل از تعداد اسلات | ## ۵. UI این تسک صفحه نمی‌سازد (تسک ۱۰ می‌سازد). | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۵.۱ | پیام‌های `forbid` فارسی و قابل فهم بیمار | ✅ | `reason` دلخواه؛ نبودنش → «قانون «X» این عملیات را مجاز نمی‌داند» | | ۵.۲ | خطای `require_resource` بدون منبع شامل نام نقش | ✅ | نام نقش، شعبه **و نام قانون** — بین ده قانون فعال، حدس زدن کار اپراتور نیست | ## ۶. تست | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۶.۱ | همهٔ عملگرها × نوع‌ها، `all`/`any`، فیلد ناموجود | ✅ | یازده حالت با dataProvider: هر شش عملگر روی هر دو نتیجه. یادداشت قبلی: تست واحدِ هر شش عملگر ندارد | | ۶.۲ | جدول ترکیب | ✅ | max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم) | | ۶.۳ | حل تناقض | ✅ | اختصاصی‌بودن و اولویت هر دو | | ۶.۴ | `spacing` — بازهٔ ممنوعه + کوئری ثابت | ✅ | `testSpacingRejectsABookingTooCloseToTheLastOne` — نزدیک رد، دور می‌گذرد | | ۶.۵ | نسخه‌بندی (قانون پنجم) | ✅ | ⭐ `testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt` | | ۶.۶ | یکپارچگی چند دسته end-to-end | ✅ | timing · selection · pricing | | ۶.۷ | هر فیلد schema قابل extract است | ✅ | ⭐ `PolicyFieldCoverageTest` — فیلدی که هیچ‌جا پر نشود، قانونی می‌سازد که بی‌صدا هرگز مطابقت نمی‌کند | | ۶.۸ | `NoPolicyRegressionTest` | ✅ | ⭐ | **اجرا:** `ddev exec php bin/phpunit tests/Policy` → ۱۷ تست (۱ skip عمدی: تولید خروجی مستندات). ## ۷. مستندات | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۷.۱ | `docs/api/policy.md` | ✅ | JSON واقعی از اجرای `DocsCaptureTest` | | ۷.۲ | قاعدهٔ «`DiscountRule` یا `Policy`؟» | ✅ | ⭐ جدول تصمیم در `policy-engine.md` | | ۷.۳ | `docs/architecture/policy-engine.md` | ✅ | حل تناقض، ترکیب، دلیل ممنوعیت کد دلخواه، دلیل عدم مهاجرت، انحراف‌ها | | ۷.۴ | یادداشت متقابل در docs مصرف‌کننده‌ها | ✅ | `pricing.md` · `appointment-plan.md` · `appointment-booking.md` · `clinic-services.md` | ## ۸. بازبینی پایانی | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ✅ | ۶ مورد ⏳ همه با تسک مقصد | | ۸.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۲۳۷ تست | | ۸.۳ | `--group=slot-mode-frozen` سبز | ✅ | | | ۸.۴ | `AvailabilityPerformanceTest` سبز | ✅ | | | ۸.۵ | `phpstan` بدون خطای جدید | ✅ | ۱۴ خطا = همان baseline | | ۸.۶ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | این تسک هیچ فایل فرانتی عوض نکرد | | ۸.۷ | تست‌های tenant سبز | ✅ | | | ۸.۸ | `docs/api/*` به‌روز | ✅ | | | ۸.۹ | دو کلاینت دیگر بررسی شدند | ✅ | با graphify بررسی شدند؛ هیچ‌کدام اندپوینت قانون را مصرف نمی‌کنند. یادداشت قبلی: هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در `breakdown.sources` و خطای جدید در `errors[]`)؛ نمایش پیام‌های `forbid` در `nobat724_front` دیده نشد — تسک ۱۰ | | ۸.۱۰ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا | | ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |