Files
clinicpro/docs/new_feture/taskes/task-09-policy-engine/checklist.md
T
hamedandClaude Opus 5 e530ab5678 docs: record the dark-mode finding on the public checklist
The wiring is fixed and browser-verified, but components/appointment/ has no
dark: utilities at all, so the booking flow still renders identically in either
theme. That is design work, and the row says so rather than claiming done.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:15:25 +03:30

12 KiB
Raw Blame History

چک‌لیست — تسک ۰۹ (موتور قوانین شش‌دسته‌ای)

وضعیت کلی: تمام‌شده با انحراف‌های ثبت‌شده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹

قواعد: _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 ساخته شد و همان بدو ورود اولین موردش را گرفت: 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() — کوئری نه حلقه ⚠️ 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()، روی سرویس بی‌الگو هم
۲.۳ تسک ۰۶ — AvailabilityEngineSpacing ⚠️ جایش 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 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 . دو کامیت جدا
۸.۱۱ موارد به‌تعویق با دلیل و تسک مقصد تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴