Two gaps against the spec. Resources could not be categorised at all — only services carried a catalog category — so "this device is for hands and feet" was unsayable. And CatalogCategory::$parent is a tree built for menu ordering: one parent per category. Laser areas overlap, so "hand" belongs under both "whole body" and "upper limb" at once, which a tree cannot express. Containment is therefore a separate directed acyclic graph (catalog_category_includes) sitting beside the display hierarchy, and resources join the existing clinic-wide categories through a many-to-many rather than growing a parallel list of their own. CategoryClosureResolver walks it transitively: whole body includes lower body includes foot, so whole body includes foot without anyone writing that pair down. The walk reads every edge of the environment in one query and traverses in memory — a query per level would tie round-trips to graph depth. The visited set doubles as the cycle guard, so even data that already contains a loop cannot hang the traversal, and assertNoCycle refuses to create one. Selection now rejects picking an area together with a category that contains it: "whole body laser" and "hand laser" in one appointment is a 422 with a Persian message naming both. This replaces hand-written incompatible_with pairs for the area case — defined once on the category instead of per item pair — while that relation stays for incompatibilities that have nothing to do with areas. Nine tests, including the two-parents case a tree could not hold, the cycle refusal, the self-edge, and the empty-graph boundary. TenantSchemaCoverageTest caught the new edge entity as unclassified; it is registered as an aggregate child of the parent category, which is what the constructor already enforces. Suite 1286 green, phpstan at its 14-error baseline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
تسکهای موتور نوبتدهی چندمنبعی Clinic Pro
پیادهسازی تدریجی clinic-pro-mostanad-sade.md روی کد موجود. گزارش وضعیت فعلی و تحلیل شکاف: 00-current-state-report.md
⛔ سه قاعدهای که پیش از هر تسکی باید بخوانی
| سند | چه میگوید |
|---|---|
| _shared/red-lines.md | منطق اسلاتی به هیچ عنوان دستکاری نمیشود · نوبتدهی سرویسی در همین فاز کامل میشود |
| _shared/ui-conventions.md | هر صفحه یا بخش جدید عیناً با دیزاینسیستم موجود — هیچ طراحی جدید |
| _shared/definition-of-done.md | هیچ تسکی بدون تکمیل چکلیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |
در تناقض، این سه سند بر متن تسکها برندهاند.
لیست تسکها
فاز ۰ — تثبیت وضعیت فعلی (پیشنیاز بقیه)
| تسک | ماژول | پروژه | Endpoint | وابستگی | زمان |
|---|---|---|---|---|---|
| ۰۰ | تکمیل نوبتدهی سرویسی | clinicpro |
۴ | — | ۱۴-۱۸h |
| ۰۰ب | سازگارسازی سایت عمومی | nobat724_front |
— | ۰۰ | ۱۰-۱۴h |
فاز ۱ — هستهٔ چندمنبعی
| تسک | ماژول | Endpoint | وابستگی | زمان |
|---|---|---|---|---|
| ۰۱ | شعبه و اتاق | ۸ | ۰۰ | ۱۰-۱۲h |
| ۰۲ | منبع، نوع منبع، مهارت، استخر | ۱۴ | ۰۱ | ۱۴-۱۸h |
| ۰۳ | تقویم منبع، مرخصی، تعطیلات ملی | ۹ | ۰۱، ۰۲ | ۱۲-۱۴h |
| ۰۴ | کاتالوگ خدمات v2 (گروه آیتم، دو نوع زمان) | ۱۰ | ۰۱ | ۱۴-۱۶h |
| ۰۵ | بخشهای نوبت و سازندهٔ برنامه | ۳ | ۰۲، ۰۴ | ۱۶-۲۰h |
| ۰۶ | موتور جستجوی وقت چندمنبعی | ۲ | ۰۳، ۰۵ | ۲۰-۲۴h |
| ۰۷ | رزرو موقت و ثبت نهایی چندمنبعی | ۴ | ۰۶ | ۱۶-۲۰h |
| ۰۸ | لیست قیمت بازهدار و snapshot فاکتور | ۷ | ۰۴، ۰۷ | ۱۲-۱۴h |
فاز ۲ — قوانین
| تسک | ماژول | Endpoint | وابستگی | زمان |
|---|---|---|---|---|
| ۰۹ | موتور قوانین ششدستهای | ۶ | ۰۵، ۰۶، ۰۸ | ۲۰-۲۴h |
| ۱۰ | فرم ساخت قانون + محیط آزمایش | ۲ | ۰۹ | ۱۰-۱۲h |
فاز ۳ — کسبوکار
| تسک | ماژول | Endpoint | وابستگی | زمان |
|---|---|---|---|---|
| ۱۱ | پکیج و دفتر اعتبار جلسات | ۸ | ۰۸ | ۱۰-۱۲h |
| ۱۲ | دوره درمان | ۹ | ۰۷، ۱۱ | ۱۶-۲۰h |
| ۱۳ | سیاست لغو، عدم حضور، لیست انتظار | ۷ | ۰۷ | ۱۰-۱۲h |
فاز ۴ — بهینهسازی
| تسک | ماژول | Endpoint | وابستگی | زمان |
|---|---|---|---|---|
| ۱۴ | رویدادهای دامنه و گزارش بهرهوری | ۳ | ۰۷ | ۸-۱۰h |
مجموع endpoint جدید: ~۹۶ · مجموع زمان: ۲۱۴ تا ۲۵۶ ساعت
ساختار هر تسک
task-XX-name/
├── task.md ← شرح، دامنه، endpoint ها، معیار پذیرش، زمان
├── architecture.md ← فایلها، entity ها، سرویسها، لایهها، قواعد UI
├── database.md ← جداول، ستونها، ایندکسها، migration
├── implementation_notes.md ← نکات فنی، edge case، سازگاری عقبرو، تست
├── checklist.md ← ☑️ وضعیت هر مورد: ✅ 🔄 ⏳ ⚠️
└── user_flow.md ← (تسکهای پیچیده) جریان کاربری
checklist.md اجباری است و ساختار ثابتی دارد:
۰. خط سرخها ۱. بکاند ۲. دیتابیس ۳. UI ۴. تست ۵. مستندات ۶. بازبینی پایانی
چکلیست — قواعد
| نماد | معنی | اجازهٔ باقیماندن در پایان تسک |
|---|---|---|
| ✅ | انجامشده و تأییدشده | بله |
| 🔄 | در حال انجام | نه |
| ⏳ | انجامنشده | نه — مگر با دلیل مکتوب و تسک مقصد |
| ⚠️ | نیازمند بررسی یا تست | نه — باید تعیین تکلیف شود |
پیش از پایان هر تسک، همهٔ ردیفهای چکلیست بازبینی میشوند و وضعیت نهایی میگیرند. هر تسک بخش «۶. بازبینی پایانی» دارد که تستها، مستندات، UI و دو کلاینت دیگر را میسنجد.
ترتیب پیشنهادی اجرا
۰۰ ── ۰۰ب
│
├─ ۰۱ ─┬─ ۰۲ ── ۰۳ ─┐
│ └─ ۰۴ ── ۰۵ ─┴─ ۰۶ ── ۰۷ ─┬─ ۰۸ ─┬─ ۰۹ ── ۱۰
│ │ └─ ۱۱ ── ۱۲
│ ├─ ۱۳
│ └─ ۱۴
فاز ۰ اختیاری نیست. اگر حالت resource (تسک ۰۶) روی حالت service نیمهکاره ساخته
شود، هر باگ موجود سرویسی به موتور جدید ارث میرسد و تشخیص منبعش غیرممکن میشود.
فاز اول قابل عرضه: ۰۰ تا ۰۸ — بعد از آن یک کلینیک زیبایی با اتاق، دستگاه و اپراتور میتواند واقعاً نوبت بگیرد.
سه حالت نوبتدهی
| حالت | مقدار WeeklySchedule.meta.booking_mode |
وضعیت |
|---|---|---|
| اسلاتی | slot |
⛔ قفل — پیشفرض، تولیدی، دستنخورده |
| سرویسی | service |
🔄 موجود ولی نیمهکاره → تسک ۰۰ و ۰۰ب کاملش میکنند |
| چندمنبعی | resource |
⏳ جدید — تسک ۰۶ به بعد |
booking_mode پس از اولین ثبت قفل میشود (WeeklySchedule::getStoredBookingMode()).
تسک ۰۶ یک استثنای کنترلشده اضافه میکند: ارتقای یکطرفه از slot/service به
resource، مشروط بر نبودِ نوبت فعال آینده. بازگشت ممنوع.
ماتریس کامل «کدام endpoint در کدام حالت» در docs/architecture/booking-modes.md
(تسک ۰۰ میسازد، تسک ۰۶ حالت سوم را اضافه میکند).
اجبار خودکار خط سرخ
هر تسک باید این را سبز نگه دارد:
ddev exec php bin/phpunit --group=slot-mode-frozen
تسک ۰۰ این تست و سه fixture آن را میسازد. fixture ها بعد از آن read-only اند: اگر تستی قرمز شد، کد باید برگردد، نه fixture.
قواعد مشترک همهٔ تسکها
از CLAUDE.md و docs/architecture/tenancy.md. فهرست کامل در _shared/definition-of-done.md:
- entity جدید یا
TenantOwnedTraitمیگیرد یا با دلیل درGlobalTablesثبت میشود entity_type, entity_idستونهای اول هر ایندکس ترکیبیِ لیست- هر uuid از request با
TenantOwnershipCheckerسنجیده میشود - timestamp ها
intیونیکس؛ نمایش شمسی فقط در UI - کنترلر نازک ·
BaseController·success/paginated/error - API جدید فقط وقتی هیچ endpoint موجودی کافی نباشد — دلیلش نوشته شود
- تست موفق + خطا + مرزی؛ بدون اجرای موفق تست، تسک تمام نیست
- هر endpoint جدید یا تغییریافته →
docs/api/*در همان نشست - رشتههای UI فارسی از i18n؛ کد و کامیت و مستندات انگلیسی
- سازگاری عقبرو:
nobat724_frontوclinic-pro-tauriدر build خطا نمیدهند — بررسی دستی اجباری است (ردیف بازبینی پایانی هر تسک) - اول commit، بعد
graphify update .