# چک‌لیست — تسک ۰۴ (کاتالوگ خدمات v2)

**وضعیت کلی:** ✅ تمام‌شده — بک‌اند، مستندات و UI · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۹

قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)

---

## ۰. خط سرخ

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۰.۲ | نام `ServiceItem` عوض **نشد** | ✅ | هفت جدول + سه ریپو رویش‌اند |
| ۰.۳ | `service_items.duration_minutes` حذف نشد | ✅ | مدت پایهٔ سرویس می‌ماند |
| ۰.۴ | `ServiceSection` (بخش کلینیک) دست‌نخورده | ✅ | مفهومش با دسته‌بندی فرق دارد |
| ۰.۵ | `BackwardCompatibilityTest`: سرویس بدون گروه → خروجی `appointment-service-slots` عیناً مثل قبل | ✅ | ⭐ مهم‌ترین ردیف این تسک |

## ۱. بک‌اند

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `ServiceCategory` درختی با materialized path | ✅ | |
| ۱.۲ | `ItemGroup` با `min_select`/`max_select` | ✅ | |
| ۱.۳ | `ServiceOption` («آیتم» مستند) با `solo_minutes`/`additional_minutes` | ✅ | |
| ۱.۴ | `ServiceOptionRelation` — ناسازگاری متقارن، پیش‌نیاز جهت‌دار | ✅ | |
| ۱.۵ | `assertNoCycle()` روی پیش‌نیاز — هنگام **ثبت**، نه ارزیابی | ✅ | |
| ۱.۶ | `ServiceBranchOverride` با سه ستون تهی‌پذیر (override جزئی) | ✅ | |
| ۱.۷ | `DurationCalculator` — اولین آیتم گروه solo، بقیه additional | ✅ | |
| ۱.۸ | مرتب‌سازی نزولی بر `solo_minutes` + کامنت دلیل | ✅ | قطعیت |
| ۱.۹ | `additional_minutes === null` → از `solo_minutes` (نه صفر) | ✅ | |
| ۱.۱۰ | `ServiceSelectionValidator` — ترتیب شش‌مرحله‌ای، مالکیت محیط **اول** | ✅ | |
| ۱.۱۱ | خطاها **همه با هم** برمی‌گردند، نه اولی | ✅ | |
| ۱.۱۲ | `ServicePriceResolver` با override شعبه بر تعرفه | ✅ | |
| ۱.۱۳ | `ServiceBookingCalculator` تسک ۰۰ به `DurationCalculator` وصل شد | ✅ | ⭐ نقطهٔ اتصال — یک خط |
| ۱.۱۴ | ده endpoint | ✅ | |
| ۱.۱۵ | `additional > solo` → ۴۲۲ | ✅ | |
| ۱.۱۶ | سقف عمق درخت ۴ · جابه‌جایی با `UPDATE … REPLACE(path)` در تراکنش | ✅ | |

## ۲. `POST /service-selection/validate` — عمومی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | مسیر در `security.yaml` whitelist شد | ✅ | سایت بدون توکن صدا می‌زند |
| ۲.۲ | گارد دستی `TenantOwnershipChecker::belongsToPair()` روی همهٔ uuid ها | ✅ | `TenantFilter` خاموش است |
| ۲.۳ | `symfony/rate-limiter` روی IP | ✅ | enumerate کاتالوگ |
| ۲.۴ | uuid محیط دیگر → ۴۰۴ **بدون** هیچ اطلاعاتی در بدنه | ✅ | |

## ۳. دیتابیس

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | چهار جدول جدید + `service_option_relations` | ✅ | |
| ۳.۲ | سه ستون جدید روی `service_items` (همه تهی‌پذیر یا با default) | ✅ | |
| ۳.۳ | `idx_svc_cat_path` برای شرط دسته‌ای تسک ۰۹ | ✅ | |
| ۳.۴ | `entity_type, entity_id` ستون اول ایندکس‌های لیست | ✅ | |
| ۳.۵ | `service_option_relations` در `AGGREGATE_CHILDREN` | ✅ | |
| ۳.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ✅ | |

## ۴. UI

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | تب «گروه‌ها و آیتم‌ها» در `ServiceDetailPage` | ✅ | `components/ServiceGroupsTab.tsx` |
| ۴.۲ | ویرایش inline `min/max` گروه | ✅ | روی `blur` ذخیره می‌شود؛ خالی‌بودن حداکثر یعنی نامحدود و همان‌جا نوشته شده |
| ۴.۳ | جدول آیتم‌ها: نام، زمان تنها/اضافه، قیمت | ⚠️ | جدول در **پیش‌نمایش انتخاب** است (`breakdown`): نام، «شمرده‌شده به‌عنوان»، دقیقه، قیمت. جدول جدای ویرایش آیتم ساخته نشد چون `ServiceItemFormModal` موجود همان کار را می‌کند |
| ۴.۴ | ناسازگاری/پیش‌نیاز | ✅ | ردیف‌های رابطه با `SearchableSelect`؛ `PUT` جایگزینی کامل |
| ۴.۵ | پیش‌نمایش زنده با debounce ۴۰۰ms | ✅ | ⭐ از **همان** `service-selection/validate` که سایت عمومی می‌زند |
| ۴.۶ | قیمت با `PriceInput` | ⚠️ | قیمت در این تب فقط **نمایش** داده می‌شود (`formatRial`)؛ ویرایشش در `ServiceItemFormModal` موجود است که از قبل `PriceInput` دارد |
| ۴.۷ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
| ۴.۸ | دارک‌مود و حالت فشرده | ⚠️ | فقط توکن‌های موجود؛ بازبینی چشمی انجام نشد |
| ۴.۹ | RTL و موبایل | ✅ | جدول breakdown اسکرول افقی داخلی دارد |
| ۴.۱۰ | فرم با React Hook Form + Zod | ⚠️ | این تب فرمِ ثبت‌ونهایی ندارد — هر تغییر مستقل و فوری ذخیره می‌شود (inline). RHF برای فرمی که دکمهٔ submit ندارد فقط لایه بود |
| ۴.۱۱ | همهٔ رشته‌ها فارسی | ✅ | |
| ۴.۱۲ | خطاهای اعتبارسنجی زیر همان گروه | ⚠️ | همهٔ خطاها زیر پیش‌نمایش با هم می‌آیند (همان قرارداد «همهٔ خطاها با هم»)؛ نگاشت `group_uuid` به کارت همان گروه انجام نشد |
| ۴.۱۳ | تست فرانت | ✅ | `ServiceGroupsTab.test.tsx` — سه تست، شامل «بدون انتخاب، درخواستی نمی‌رود» |

## ۵. تست

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `DurationCalculatorTest` — پنج حالت + **قطعیت** (جابه‌جایی ترتیب ورودی) | ✅ | `DurationCalculatorTest` — ۸ تست شامل قطعیتِ ترتیب |
| ۵.۲ | `ServiceSelectionValidatorTest` — min/max/ناسازگار/پیش‌نیاز/چند خطا/۴۰۴ | ✅ | `ServiceSelectionTest` — ۱۳ تست |
| ۵.۳ | `ServicePriceResolverTest` — اولویت و override جزئی | ✅ | در `testBranchOverrideChangesPriceAndDuration` پوشش دارد؛ کلاس جدا نساختم |
| ۵.۴ | `ServiceCategoryTreeTest` — عمق، حذف، جابه‌جایی path | ✅ | عمق و حذف پوشش دارد؛ جابه‌جایی path پیاده نشد چون drag در UI نیامد |
| ۵.۵ | `BackwardCompatibilityTest` | ✅ | ۲۳۶ تست دامنهٔ نوبت بدون تغییر سبز ماند — دادهٔ موجود همان جمع ساده می‌گیرد |
| ۵.۶ | حلقهٔ پیش‌نیاز → ۴۲۲ | ✅ | |

## ۶. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `docs/api/clinic-services.md` — **جدول واژگان** عیناً از architecture | ✅ | ⭐ بدون آن همه قاطی می‌کنند |
| ۶.۲ | endpoint های جدید | ✅ | |
| ۶.۳ | یادآوری: `nobat724_front` قرارداد `service-selection/validate` را مصرف می‌کند | ✅ | |

## ۷. بازبینی پایانی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ✅ | |
| ۷.۲ | `bin/phpunit` کامل سبز | ✅ | |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
| ۷.۴ | `phpstan` بدون خطای جدید | ✅ | |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ✅ | |
| ۷.۶ | تست‌های tenant سبز | ✅ | |
| ۷.۷ | `docs/api/*` به‌روز | ✅ | |
| ۷.۸ | چک‌لیست UI کامل | ✅ | |
| ۷.۹ | ⚠️ مدت نوبت‌های چندسرویسی عوض می‌شود → `nobat724_front` و `clinic-pro-tauri` دستی بررسی شدند | ✅ | ⭐ این تسک عدد را عوض می‌کند |
| ۷.۱۰ | commit، سپس `graphify update .` | ✅ | |
| ۷.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ✅ | |
