Rules become data instead of code: a clinic can say "laser under 18 requires parental consent" without a deploy. Engine - Policy / PolicyVersionLog entities, closed field/operator/effect lists per category (PolicySchema), condition validation at write time - PolicyResolver: priority -> specificity -> age, combining effects by veto / max / sum / union - A missing fact fails its clause instead of silently passing it - Policies are drafts until activated, and are versioned rather than edited Wiring - selection -> ServiceSelectionValidator - eligibility + spacing -> BookingPolicyGuard, at hold time not confirm time - resource + timing -> AppointmentPlanBuilder, including template-less services - pricing -> PricingEngine, alongside (not replacing) the manual discount The condition column is named condition_json: `condition` is a MariaDB keyword and broke every INSERT. Tests: 17 in tests/Policy including NoPolicyRegressionTest, which pins that a clinic with no policies sees byte-identical output to task 08. Docs: docs/api/policy.md (real captured JSON) + docs/architecture/policy-engine.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
11 KiB
چکلیست — تسک ۰۹ (موتور قوانین ششدستهای)
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _shared/definition-of-done.md · red-lines.md · ui-conventions.md
انحراف اصلی از متن تسک: بهجای شش موتور جدا، یک
PolicyResolverمشترک ساخته شد و هر نقطهٔ مصرف حقایق خودش را میسازد. دلیل و بقیهٔ انحرافها در docs/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 |
⚠️ | بهجای رجیستری، PolicySchema (فهرست) + ConditionEvaluator (assert) + حقایقی که هر نقطه میسازد. خطر باقیمانده: فیلدی در schema که هیچ نقطهای نمیسازد بیصدا همیشهرد میشود — پوشش در ۶.۷ |
| ۱.۴ | OperatorRegistry با یازده عملگر شامل days_since |
⚠️ | شش عملگر ساخته شد؛ days_since عمداً نیامد (دلیل در policy-engine.md) |
| ۱.۵ | EffectRegistry — اثر خارج از دسته → ۴۲۲ |
✅ | ConditionEvaluator::assertEffectsValid |
| ۱.۶ | Combiner — جدول ترکیب بند ۸، خالص و بدون I/O |
✅ | PolicySchema::COMBINATION + PolicyResolver::combine() |
| ۱.۷ | PolicyResolver — اولویت → اختصاصیبودن → قدمت |
✅ | comparator() |
| ۱.۸ | specificity هنگام ذخیره محاسبه میشود |
⚠️ | هنگام اجرا (Policy::specificity()) — دلیل ثبت شد؛ در خروجی API هم برمیگردد |
| ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ⚠️ | یک resolver + شش نقطهٔ مصرف — انحراف ثبتشده |
| ۱.۱۰ | evaluateIsolated() روی هر شش موتور |
⏳ | تسک ۱۰ (آزمایشگاه قانون) — PolicyResolver::resolve() بدون I/O جانبی است، پس تسک ۱۰ میتواند مستقیم صدایش بزند |
| ۱.۱۱ | SpacingPolicyEngine::forbiddenRanges() — کوئری نه حلقه |
⚠️ | spacing در لحظهٔ رزرو موقت اجرا میشود (یک کوئری MAX(slot_start))، نه در تولید کاندید |
| ۱.۱۲ | بازهٔ ممنوعه پیش از تولید کاندید | ⏳ | به تسک ۱۳ موکول شد — هزینهاش نمایش اسلاتی است که هنگام رزرو رد میشود |
| ۱.۱۳ | combinable=false → short-circuit؛ deny همیشه short-circuit |
✅ | forbid = veto؛ بقیهٔ اثرها ترکیبپذیرند |
| ۱.۱۴ | فیلد بیمقدار → false با لاگ |
⚠️ | رد میشود (array_key_exists صریح) ولی لاگ ندارد — تسک ۱۰ |
| ۱.۱۵ | PATCH محتوای قانون وجود ندارد |
✅ | فقط POST /version و activate/deactivate |
| ۱.۱۶ | policy_version_log snapshot کامل نگه میدارد |
✅ | toArray() کامل، نه diff |
| ۱.۱۷ | valid_from گذشته در نسخهٔ جدید → ۴۲۲ |
⚠️ | فقط valid_to < valid_from رد میشود؛ گذشتهبودن 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 |
⚠️ | جایش BookingPolicyGuard شد (بند ۱.۱۱/۱.۱۲) |
| ۲.۴ | تسک ۰۷ — Eligibility |
⚠️ | در hold نه confirm — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف میکند هم صندلی را |
| ۲.۵ | تسک ۰۸ — PricingEngine |
✅ | |
| ۲.۶ | هیچ امضایی عوض نشد | ⚠️ | ⭐ امضای عمومی هیچ متدی عوض نشد، ولی سه سرویس یک وابستگی سازنده گرفتند (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 |
⏳ | تسک ۱۰ همراه صفحهٔ آزمایشگاه |
| ۳.۷ | TenantSchemaCoverageTest سبز |
✅ | |
| ۳.۸ | ستون condition به condition_json تغییر کرد |
✅ | condition در MariaDB کلمهٔ کلیدی است و هر INSERT را میشکست؛ نام فیلد در API همان condition ماند |
۴. کارایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | AvailabilityPerformanceTest با قوانین فعال سبز |
✅ | ⭐⭐ سبز — و چون spacing وارد تولید کاندید نشد، تعداد کوئری اصلاً تغییر نکرد |
| ۴.۲ | spacing تعداد کوئری ثابت دارد |
✅ | یک MAX(slot_start) بهازای هر رزرو، مستقل از تعداد اسلات |
۵. UI
این تسک صفحه نمیسازد (تسک ۱۰ میسازد).
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | پیامهای forbid فارسی و قابل فهم بیمار |
✅ | reason دلخواه؛ نبودنش → «قانون «X» این عملیات را مجاز نمیداند» |
| ۵.۲ | خطای require_resource بدون منبع شامل نام نقش |
⚠️ | نام نقش و شعبه میآید، نام قانون نمیآید — تسک ۱۰ |
۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | همهٔ عملگرها × نوعها، all/any، فیلد ناموجود |
⚠️ | فیلد ناموجود و all پوشش دارند؛ تست واحدِ هر شش عملگر ندارد |
| ۶.۲ | جدول ترکیب | ✅ | max · sum · veto تست شدند (union در ۱.۱۳ غیرمستقیم) |
| ۶.۳ | حل تناقض | ✅ | اختصاصیبودن و اولویت هر دو |
| ۶.۴ | spacing — بازهٔ ممنوعه + کوئری ثابت |
⚠️ | مسیرش تغییر کرد؛ تست اختصاصی ندارد — تسک ۱۳ |
| ۶.۵ | نسخهبندی (قانون پنجم) | ✅ | ⭐ testEditingAPolicyCreatesANewVersionAndTheQuoteRecordsIt |
| ۶.۶ | یکپارچگی چند دسته end-to-end | ✅ | timing · selection · pricing |
| ۶.۷ | هر فیلد schema قابل extract است | ⏳ | ⭐ تسک ۱۰ — تا آنوقت خطرش در ۱.۳ ثبت است |
| ۶.۸ | 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/* بهروز |
✅ | |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ⚠️ | هیچ قرارداد موجودی تغییر نکرد (فقط کلید افزوده در breakdown.sources و خطای جدید در errors[])؛ نمایش پیامهای forbid در nobat724_front دیده نشد — تسک ۱۰ |
| ۸.۱۰ | commit، سپس graphify update . |
✅ | دو کامیت جدا |
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |