# تسک ۰۴ — کاتالوگ خدمات نسخهٔ ۲: گروه آیتم، دو نوع زمان، ناسازگاری **فاز:** ۱ (هسته) · **وابستگی:** ۰۱ · **زمان:** ۱۴-۱۶ ساعت --- ## هدف مستند بند ۵ می‌گوید انتخاب آیتم خودش قانون دارد و این قوانین **نباید** به موتور قوانین سپرده شوند: «حتماً یک سطح انرژی، فقط یکی»، «بین ۱ تا ۸ دندان»، «بیکینی با فول‌بادی جمع نمی‌شود». و مهم‌تر: هر آیتم دو زمان دارد — «زمان تنها» و «زمان اضافه». ## وضعیت فعلی ```php // src/ClinicService/Entity/ServiceItem.php private ?int $durationMinutes = null; // یک عدد، تخت private int $priceRials = 0; private bool $bookable = false; ``` و در `AppointmentController::serviceSlots()`: ```php $totalMinutes += $duration; // ← جمع ساده؛ همان فرمولی که مستند ردش می‌کند ``` نتیجه: بیمار که «صورت + بیکینی» می‌خواهد، ۱۵+۱۵=۳۰ دقیقه ظرفیت می‌گیرد در حالی که واقعیت ۱۵+۸=۲۳ دقیقه است. هفت دقیقه ضرب در روزی ۲۰ نوبت = یک ساعت ظرفیت هدررفته در روز. همچنین `ServiceSection` تک‌سطحی است و دسته‌بندی درختی مستند را ندارد. ## دامنه **هست:** - `ServiceCategory` درختی (جدا از `ServiceSection` موجود که «بخش کلینیک» است) - `ItemGroup` با `min_select` / `max_select` - روی `ServiceItem`: `solo_duration_minutes` و `additional_duration_minutes` - `ServiceItemRelation` برای `incompatible_with` و `requires` - `ServiceBranchOverride` برای قیمت و مدت اختصاصی شعبه - `session_count` روی سرویس (تک‌جلسه یا دوره‌ای — پروتکل کاملش تسک ۱۲) - `ServiceSelectionValidator` — اعتبارسنجی انتخاب کاربر پیش از هر محاسبه - `DurationCalculator` — محاسبهٔ درست مدت با دو نوع زمان **نیست:** بخش‌های نوبت و نیازمندی منبع (تسک ۰۵)، اعمال روی جستجوی وقت (تسک ۰۶). ## Endpoint ها | متد | مسیر | توضیح | |---|---|---| | GET | `/api/v1/service-categories/tree` | درخت دسته‌بندی | | POST/PATCH/DELETE | `/api/v1/service-category[/{uuid}]` | | | GET/POST | `/api/v1/service-item/{uuid}/groups` | گروه‌های آیتم یک سرویس | | PATCH/DELETE | `/api/v1/item-group/{uuid}` | | | PUT | `/api/v1/item-group/{uuid}/items` | جایگزینی کامل آیتم‌های گروه | | PUT | `/api/v1/service-item/{uuid}/relations` | ناسازگاری و پیش‌نیاز | | PUT | `/api/v1/service-item/{uuid}/branch-overrides` | قیمت/مدت per شعبه | | POST | `/api/v1/service-selection/validate` | اعتبارسنجی انتخاب + مدت و قیمت محاسبه‌شده | `POST /service-selection/validate` مهم‌ترین endpoint این تسک است: سایت عمومی و پنل هر دو پیش از رفتن به مرحلهٔ انتخاب زمان، آن را صدا می‌زنند. ## معیار پذیرش - ✅ موفق: سرویس «لیزر» با گروه «نواحی» (`min=1, max=8`) و آیتم‌های صورت (تنها ۱۵، اضافه ۸) و بیکینی (تنها ۱۲، اضافه ۸). انتخاب هر دو → `POST /service-selection/validate` برمی‌گرداند `total_duration_minutes = 23` (اولین آیتم زمان تنها، بقیه زمان اضافه) و `valid = true`. - ✅ موفق: انتخاب فقط بیکینی → `total_duration_minutes = 12`. - ✅ موفق: شعبهٔ مرکزی برای همین سرویس `price_rials` بالاتر دارد → با `branch_uuid` مرکزی، قیمت override اعمال می‌شود. - ❌ خطا: انتخاب صفر آیتم از گروهی با `min_select=1` → `valid=false` با `errors[{group_uuid, code: 'min_select', message: 'انتخاب حداقل یک مورد از «نواحی» الزامی است'}]`. - ❌ خطا: انتخاب ۹ آیتم از گروهی با `max_select=8` → `valid=false` با کد `max_select`. - ❌ خطا: انتخاب دو آیتم ناسازگار → `valid=false` با کد `incompatible` و نام هر دو آیتم. - ❌ خطا: انتخاب آیتمی که پیش‌نیازش انتخاب نشده → `valid=false` با کد `missing_prerequisite`. - ❌ خطا: uuid آیتم از محیط دیگر → `404` (نه ۴۲۲ — نباید وجودش لو برود). - ⚠️ مرزی: `max_select = null` یعنی نامحدود. - ⚠️ مرزی: گروه با `min_select = 0` یعنی اختیاری. - ⚠️ مرزی: آیتم بدون `additional_duration_minutes` → از `solo_duration_minutes` استفاده شود (سازگاری با داده‌های موجود که فقط یک `duration_minutes` دارند). - ⚠️ مرزی: حلقهٔ پیش‌نیاز (الف پیش‌نیاز ب، ب پیش‌نیاز الف) → `422` هنگام ثبت رابطه. ## خروجی - توسعهٔ `src/ClinicService/` (بدون شکستن endpoint های موجود) - `assets/admin/pages/ServiceDetailPage.tsx` توسعه: تب «گروه‌ها و آیتم‌ها» - `docs/api/clinic-services.md` به‌روزرسانی - migration + backfill: `duration_minutes` موجود → `solo_duration_minutes`