# چک‌لیست تسک ۱۵ — نوبت‌دهی بر پایهٔ منبع

پرامپت: [`.claude/prompt/resource-first-booking-model.md`](../../../../.claude/prompt/resource-first-booking-model.md)

وضعیت‌ها: ✅ انجام شد · 🔄 در حال انجام · ⏳ در صف · ⛔ خارج از محدوده با دلیل · ⚠️ ناقص با دلیل

---

## ۰. قواعد غیرقابل‌مذاکره

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد | ⏳ | |
| ۰.۲ | رنگ‌ها فقط از توکن‌های `styles.css` — هیچ hex خام در کد جدید | ⏳ | |
| ۰.۳ | کامپوننت از `components/ui/` — `<select>` خام صفر | ⏳ | |
| ۰.۴ | دارک‌مود · حالت فشرده · موبایل ۳۹۰px هر سه سالم | ⏳ | با اسکرین‌شات واقعی |
| ۰.۵ | `.card` با `card-pad` و برچسب با `.field-block` | ⏳ | |
| ۰.۶ | دکمهٔ بازگشت در صفحات زیرمجموعه | ⏳ | |
| ۰.۷ | وضعیت لیست در query string با `useUrlState` | ⏳ | |
| ۰.۸ | هیچ صفحه/فیلد/endpointی خارج از سند اضافه نشد | ⏳ | |
| ۰.۹ | هیچ interface/کلاس پایه بدون بیش از یک پیاده‌سازیِ فعلی | ⏳ | |
| ۰.۱۰ | Controller نازک · منطق در Service · کوئری در Repository | ⏳ | |

## ۱. جدول رابطهٔ منبع↔سرویس

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | entity `ResourceServiceOffering` با `TenantOwnedTrait` | ⏳ | |
| ۱.۲ | قید یکتای `(resource_id, service_item_id)` | ⏳ | |
| ۱.۳ | ایندکس `(entity_type, entity_id, service_item_id)` | ⏳ | |
| ۱.۴ | `durationMinutes` و `priceRials` تهی‌پذیر = ارث | ⏳ | |
| ۱.۵ | migration ساخته و اجرا شد | ⏳ | |
| ۱.۶ | تست: ثبت ردیف | ⏳ | |
| ۱.۷ | تست: جفت تکراری → خطای یکتایی | ⏳ | |
| ۱.۸ | تست: جفت محیط از منبع مشتق می‌شود نه از ورودی | ⏳ | |

## ۲. Resolver مدت و قیمت

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `ResourceServiceResolver` با زنجیرهٔ چهارسطحی | ⏳ | |
| ۲.۲ | `ResolvedServiceSpec` منبعِ هر مقدار را می‌گوید | ⏳ | |
| ۲.۳ | مدت و قیمت **جدا** حل می‌شوند | ⏳ | |
| ۲.۴ | تست سطح ۱ (منبع+گزینه) | ⏳ | |
| ۲.۵ | تست سطح ۲ (منبع+سرویس) | ⏳ | |
| ۲.۶ | تست سطح ۳ (شعبه) | ⏳ | |
| ۲.۷ | تست سطح ۴ (پیش‌فرض آیتم) | ⏳ | |
| ۲.۸ | تست مرزی: مدت از سطح ۱، قیمت از سطح ۳ | ⏳ | |

## ۳. فیلتر کاندیدها بر اساس سرویس

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `findEligible()` آرگومان `?ServiceItem` گرفت | ⏳ | |
| ۳.۲ | سازگاری عقب‌رو: بدون ردیف = بدون فیلتر | ⏳ | |
| ۳.۳ | `AppointmentPlanBuilder` سرویس را پاس می‌دهد | ⏳ | |
| ۳.۴ | تست: دو منبع، یکی وصل → assignment همان یکی | ⏳ | |
| ۳.۵ | تست: غیرفعال‌کردن ردیف → slots خالی با reason | ⏳ | |

## ۴. منبع و snapshot روی نوبت

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `Appointment::$resource` تهی‌پذیر | ⏳ | |
| ۴.۲ | `Appointment::$serviceOptionItem` تهی‌پذیر | ⏳ | |
| ۴.۳ | `service_total_minutes` از resolver پر می‌شود | ⏳ | |
| ۴.۴ | `PriceSnapshot` از resolver پر می‌شود | ⏳ | |
| ۴.۵ | `toArray()` منبع و گزینه را برمی‌گرداند | ⏳ | |
| ۴.۶ | migration بدون شکستن نوبت‌های موجود | ⏳ | |
| ۴.۷ | تست: تغییر قیمت سرویس، snapshot قدیمی ثابت | ⏳ | |

## ۵. رزرو با منبع در endpointها

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `POST /api/v1/appointment` فیلد `resource_uuid` می‌گیرد | ⏳ | |
| ۵.۲ | با منبعِ پزشک، پزشک استنتاج می‌شود | ⏳ | |
| ۵.۳ | `Appointment.doctor` تهی‌پذیر شد (منبع دستگاهی) | ⏳ | |
| ۵.۴ | `GET /api/v1/resource/{uuid}/services` با مقادیر حل‌شده | ⏳ | |
| ۵.۵ | curl: رزرو با `resource_uuid` → ۲۰۱ | ⏳ | |
| ۵.۶ | curl: مسیر قدیمی `doctor_uuid` سالم ماند | ⏳ | |
| ۵.۷ | curl: منبع بی‌ارتباط → ۴۲۲ | ⏳ | |
| ۵.۸ | curl: بدون هیچ‌کدام → ۴۲۲ | ⏳ | |

## ۶. پنل ادمین — تب سرویس‌های منبع

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | تب روی صفحهٔ منبع موجود، بدون صفحهٔ جدید | ⏳ | |
| ۶.۲ | `DataTable` + `StatusBadge` + `ConfirmDialog` | ⏳ | |
| ۶.۳ | مقدار مؤثر به‌صورت placeholder با برچسب منبعش | ⏳ | |
| ۶.۴ | فرم با React Hook Form + Zod، داده با TanStack Query | ⏳ | |
| ۶.۵ | تست: نمایش مقدار مؤثر | ⏳ | |
| ۶.۶ | تست: ذخیرهٔ override | ⏳ | |
| ۶.۷ | تست: پاک‌کردن override → بازگشت به ارث | ⏳ | |
| ۶.۸ | اسکرین‌شات دارک‌مود | ⏳ | |
| ۶.۹ | اسکرین‌شات حالت فشرده | ⏳ | |
| ۶.۱۰ | اسکرین‌شات موبایل ۳۹۰px بدون اسکرول افقی | ⏳ | |

## ۷. دسته‌بندی سراسری با «شامل بودن»

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `resource_catalog_categories` (m2m منبع↔دسته) | ⏳ | |
| ۷.۲ | `catalog_category_includes` (یال DAG) | ⏳ | |
| ۷.۳ | `CategoryClosureResolver::descendants()` با محافظ دور | ⏳ | |
| ۷.۴ | تعارض انتخاب در `ServiceSelectionValidator` → ۴۲۲ فارسی | ⏳ | |
| ۷.۵ | تقدم منابعِ پوشش‌دهندهٔ دسته در `findEligible` | ⏳ | |
| ۷.۶ | تست: «تمام بدن» → دست/پا | ⏳ | |
| ۷.۷ | تست: بستار گذرا سه‌سطحی | ⏳ | |
| ۷.۸ | تست: یال دوری → ۴۲۲ | ⏳ | |
| ۷.۹ | تست: دستهٔ بی‌یال → آرایهٔ خالی | ⏳ | |
| ۷.۱۰ | curl: «تمام بدن + دست» با هم → ۴۲۲ | ⏳ | |

## ۸. حذف زیرسیستم‌های خارج از مدل

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | `src/Policy/` حذف شد | ✅ | `src/Policy/` (۲۷ فایل) حذف شد |
| ۸.۲ | `src/Package/` حذف شد | ✅ | `src/Package/` (۱۳ فایل) حذف شد |
| ۸.۳ | `src/Course/` حذف شد | ✅ | `src/Course/` (۱۴ فایل) حذف شد |
| ۸.۴ | `src/Cancellation/` + `src/Waitlist/` حذف شد | ✅ | `src/Cancellation/` + `src/Waitlist/` (۱۹ فایل) حذف شد |
| ۸.۵ | `src/Report/` + `src/Shared/Event/` حذف شد | ✅ | `src/Report/` + `src/Shared/Event/` حذف شد |
| ۸.۶ | قلاب‌ها از `AppointmentPlanBuilder` کنده شد | ✅ | `applyTimingPolicies`/`applyResourcePolicies`/`requirementForRole` برداشته شد — ۴۴۹ → ۳۷۹ خط |
| ۸.۷ | قلاب‌ها از `PricingEngine` کنده شد | ✅ | تخفیف سیاستی و پوشش پکیج از `PricingEngine` و `PriceQuote` رفت |
| ۸.۸ | قلاب‌ها از `BookingController` و `BookingService` کنده شد | ✅ | `BookingPolicyGuard`، `PackageConsumption`، `CreditLedger`، `CourseSessionLinker` و رویدادها کنده شد |
| ۸.۹ | قلاب‌ها از `ServiceSelectionValidator` کنده شد | ✅ | `policyErrors()` از validator رفت؛ گروه و رابطه دست‌نخورده ماند |
| ۸.۱۰ | `GlobalTables` و seeder پاک‌سازی شد | ✅ | `GlobalTables`، سیدر، `Schedule.php` و `messenger.yaml` پاک شد |
| ۸.۱۱ | صفحات پنل و مسیرهای `App.tsx` حذف شد | ✅ | ۱۱ صفحهٔ پنل + مسیرها + منوی تنظیمات + هوک‌ها + تایپ‌ها حذف شد |
| ۸.۱۲ | migration `DROP TABLE` با `down()` واقعی | ✅ | `Version20260801170043` — ۱۵ جدول با `IF EXISTS`، ستون `course_session_id` نوبت |
| ۸.۱۳ | `debug:router` صفر مسیر حذف‌شده | ✅ | ۵ مسیر باقی‌مانده مربوط به «پکیج انبار» و «گزارش بیمه» است، نه دامنه‌های حذف‌شده |
| ۸.۱۴ | تست‌های دامنه‌های حذف‌شده پاک شد | ✅ | `tests/{Policy,Package,Course,Cancellation,Waitlist,Report}` + assertionهای رویداد در دو تست |

## ۹. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۹.۱ | `docs/api/appointment.md` — `resource_uuid` و پاسخ جدید | ⏳ | |
| ۹.۲ | `docs/api/appointment.md` — بخش‌های حذف‌شده پاک شد | ⏳ | |
| ۹.۳ | `docs/api/clinic.md` — endpoint سرویس‌های منبع | ⏳ | |
| ۹.۴ | سند معماری مدل منبع‌محور | ⏳ | |
| ۹.۵ | چک‌لیست تسک‌های ۹ تا ۱۴ با وضعیت «حذف‌شده» | ⏳ | |
| ۹.۶ | `TEST_USERS.md` به‌روز شد | ⏳ | |

## ۱۰. تأیید نهایی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱۰.۱ | `ddev exec php bin/phpunit` کامل سبز | ⏳ | |
| ۱۰.۲ | `--group=slot-mode-frozen` سبز | ⏳ | خط قرمز |
| ۱۰.۳ | `phpstan` روی baseline ۱۴ خطا | ⏳ | |
| ۱۰.۴ | `npx tsc --noEmit` بدون خطا | ⏳ | |
| ۱۰.۵ | `npx vitest run assets/admin` سبز | ⏳ | |
| ۱۰.۶ | `app:seed-scenarios --reset -n` بدون خطا | ⏳ | |
| ۱۰.۷ | کامیت + `graphify update` + کامیت گراف | ⏳ | |
