docs/architecture/resource-first-model.md describes the shape: the three entities, why an option is a ServiceItem rather than a fourth table, the four-level resolution chain, the two conditions on the eligibility filter and what each of them prevented, and why containment is a graph beside the display tree rather than the tree itself. docs/api/resource.md gains both offering endpoints with the response captured from a real call, including a row where the price comes from the branch and one where it comes from the resource — the two cases the *_source fields exist for. docs/api/appointment.md documents resource_uuid, the doctor inference, and the nullable resource/service_option in the response. The checklists for tasks 9 to 14 keep their rows but open with a banner saying the task was removed, when, by whose decision, and which commit to revert. They are history now; deleting them would erase the record of work that shipped and was then withdrawn. Verified end to end: 1304 tests, slot-mode-frozen green, phpstan at 14, tsc clean, 648 panel tests, and app:seed-scenarios --reset builds all three environments. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
13 KiB
چکلیست — تسک ۰۹ (موتور قوانین ششدستهای)
⛔ این تسک از محصول حذف شد
تصمیم مالک محصول، ۱۴۰۵/۰۵/۱۰: مدل نوبتدهی به منبع/سرویس/گزینه محدود شد و هر چیز خارج از آن حذف شد. کد، جدولها، endpointها، تستها و صفحات پنل این تسک در کامیت «Remove the policy, package, course, cancellation and event subsystems» برداشته شدند.
ریسکش پیش از اجرا دو بار مطرح و دو بار تأیید شد. ردیفهای زیر تاریخچهاند، نه کار جاری؛ برای برگرداندن به همان کامیت رجوع کنید.
مدل جایگزین:
docs/architecture/resource-first-model.mdو چکلیست تسک ۱۵.
وضعیت کلی: ✅ تمامشده با انحرافهای ثبتشده · آخرین بازبینی: ۱۴۰۵/۰۵/۰۹
قواعد: _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() — کوئری نه حلقه |
✅ | تصمیم ثبتشده در 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 — جایش BookingPolicyGuard شد (بند ۱.۱۱/۱.۱۲) |
| ۲.۴ | تسک ۰۷ — Eligibility |
✅ | تصمیم ثبتشده در deviations.md — در hold نه confirm — رد کردن بعد از گرفتن صندلی هم وقت بیمار را تلف میکند هم صندلی را |
| ۲.۵ | تسک ۰۸ — PricingEngine |
✅ | |
| ۲.۶ | هیچ امضایی عوض نشد | ✅ | تصمیم ثبتشده در 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 . |
✅ | دو کامیت جدا |
| ۸.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ✅ | تسک ۱۰: ۱.۱۰، ۱.۱۴، ۳.۳، ۳.۶، ۵.۲، ۶.۷، ۸.۹ · تسک ۱۳: ۱.۱۲، ۶.۴ |