# چک‌لیست — تسک ۰۰ (تکمیل نوبت‌دهی سرویسی در clinicpro) **وضعیت کلی:** 🔄 در حال انجام — قابلیت ۴ از ۱۰ تمام شد **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸ قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) · خط سرخ‌ها: [_shared/red-lines.md](../_shared/red-lines.md) · UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md) --- ## ۰. خط سرخ — منطق اسلاتی | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۰.۱ | `SlotModeFrozenTest` + سه fixture ساخته شد **پیش از** هر تغییر کد | ✅ | `tests/Appointment/SlotModeFrozenTest.php` + `tests/Appointment/fixtures/` — هیچ کد تولیدی‌ای هنوز لمس نشده | | ۰.۲ | ~~fixture ها با تاریخ ثابت‌اند، نه `time()`~~ → fixture **ساختاری** است | ✅ | **انحراف عمدی از متن تسک.** تاریخ ثابتِ گذشته را `isWithinBookingWindow` رد می‌کند و snapshot خالی چیزی را تضمین نمی‌کند. به‌جایش: برنامهٔ قطعی (هر ۷ روز یک شیفت ۰۹:۰۰–۱۱:۰۰/۳۰ دقیقه) روی `+3 days`، و epoch/uuid با placeholder نرمال می‌شوند. آنچه قفل می‌شود: کلیدها، ترتیب، نوع‌ها، ساعت‌های محلی | | ۰.۳ | کامنت «read-only، هیچ تسکی به‌روزش نمی‌کند» بالای هر سه fixture | ✅ | کلید `_readme` در دو JSON (در loader حذف می‌شود) + docblock در فایل PHP | | ۰.۴ | هیچ متد موجود `SlotCalculatorService` ویرایش نشد | ✅ | فقط `getServiceStartTimes` یک پارامتر **اختیاری** با پیش‌فرض `null` گرفت. **اثبات کارکرد تور ایمنی:** تست منجمد همان لحظه قرمز شد و دقیقاً همان پارامتر را نشان داد، در حالی که دو قرارداد پاسخ سبز ماندند. fixture امضا یک بار با تاریخچهٔ مکتوب به‌روز شد (header خودش مجاز کرده) | | ۰.۵ | `GET /appointment-slots` بیت‌به‌بیت دست‌نخورده | ⏳ | در پایان تسک تأیید می‌شود | | ۰.۶ | `GET /month-availability/{doctorUuid}` دست‌نخورده | ⏳ | در پایان تسک تأیید می‌شود | | ۰.۷ | `active_slot_key` و `refreshActiveSlotKey()` دست‌نخورده | ✅ | `setIsReserve()` **وجود ندارد**؛ toggle رزرو از قبل با `rescheduleTo($start,$end,$isReserve)` انجام می‌شود که خودش `refreshActiveSlotKey()` را صدا می‌زند ([Appointment.php:316](../../../src/Appointment/Entity/Appointment.php)). یادداشت قبلی چک‌لیست غلط بود | | ۰.۸ | `isSlotTaken` امضا و معنا دست‌نخورده | ✅ | لمس نشد؛ فقط الگویش تکرار شد | | ۰.۹ | `--group=slot-mode-frozen` سبز | ✅ | `OK (3 tests, 8 assertions)` — نیازمند `#[Group]` attribute بود، نه `@group` (PHPUnit 12 annotation را حذف کرده) | | ۰.۱۰ | baseline: کل `tests/Appointment` پیش از تغییرات سبز | ✅ | `OK (166 tests, 382 assertions)` | ## ۱. بک‌اند | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۱.۱ | `ServiceBookingCalculator` ساخته شد | ✅ | `src/Appointment/Service/ServiceBookingCalculator.php` + VO در `src/Appointment/ValueObject/ServiceBookingDuration.php` (قرارداد پروژه `ValueObject` است نه `Dto` — `src/Billing/ValueObject`، `src/Insurance/ValueObject`) | | ۱.۲ | `serviceSlots()` موجود از آن استفاده می‌کند، **خروجی‌اش عوض نشده** | ✅ | ۲۰ خط از کنترلر حذف شد. بیت‌به‌بیت‌بودن: `error()` و `ExceptionSubscriber` **یک envelope یکسان** می‌سازند (`['success'=>false,'data'=>null,'errors'=>[{code,message,field?}]]`)، پس `AppException` جای `return $this->error()` نشست. شاهد: ۱۶۶ تست موجود سبز ماند، شامل `ServiceBasedSlotsTest` و `ServiceModeSectionDurationTest` که همین مسیرهای خطا را می‌سنجند | | ۱.۳ | جمع سادهٔ `+=` **حفظ شد** (اصلاحش تسک ۰۴ است) | ✅ | با کامنت صریح در docblock کلاس: «تسک ۰۴ همان‌جا یک خط عوض می‌کند» | | ۱.۴ | `ServiceRescheduleService` + `POST /appointment/{uuid}/service-reschedule` | ⏳ | | | ۱.۵ | `PATCH /appointment/{uuid}` توسعه یافت — منطق جدید داخل `isServiceMode()` | ⏳ | | | ۱.۶ | `PATCH` مقدار `service_item_uuids[]` می‌پذیرد | ⏳ | | | ۱.۷ | `ReserveConversionService` + `POST /appointment/{uuid}/convert-reserve` | ⏳ | | | ۱.۸ | نوبت رزرو در حالت سرویسی سرویس‌ها را ذخیره می‌کند | ⏳ | | | ۱.۹ | `excludeAppointmentId` روی `getServiceStartTimes` و `findBusyIntervals` | ✅ | همان نام/جای/پیش‌فرضِ `isSlotTaken($doctor,$start,$end,?int $excludeId=null)`. در repository به `occupiedIntervals()` هم پاس داده می‌شود، پس `findBlockingIntervals` بی‌تغییر ماند | | ۱.۱۰ | `Appointment::replaceServiceItems()` + `currentServiceUuids()` | ✅ | کنار `addServiceItem()` موجود؛ `currentServiceUuids()` ترتیب ورودی را حفظ می‌کند و برای نوبت قدیمیِ فقط-تکی هم uuid می‌دهد | | ۱.۱۱ | `replaceServiceItems` مقدار `serviceItem` تکی را هم‌گام می‌کند | ✅ | برخلاف `addServiceItem()` که فقط وقتی `null` است ست می‌کند، این یکی **بی‌قید** بازنویسی می‌کند (و با فهرست خالی `null` می‌شود). چهار مصرف‌کننده روی `service_item` تکی خوانده‌اند | | ۱.۱۲ | ~~`setIsReserve()` صدا زدن `refreshActiveSlotKey()`~~ | ✅ | **موردی برای انجام نبود:** `setIsReserve()` وجود ندارد؛ toggle رزرو از قبل با `rescheduleTo($start,$end,$isReserve)` است که خودش `refreshActiveSlotKey()` را صدا می‌زند | | ۱.۱۳ | اعتبارسنجی زمان با **عضویت در `getServiceStartTimes`**، نه فقط `isSlotTaken` | ⏳ | | | ۱.۱۴ | `TenantOwnershipChecker` روی همهٔ uuid های سرویس، **پیش از** هر بررسی دیگر | ✅ | `belongsToPair()` (نه `belongsTo()`): مسیر عمومی است و کاربر احراز‌شده ندارد. جای مقایسهٔ دستیِ `getSection()->getEntityType()` نشست — نقطهٔ واحد `tenancy.md`. ⚠️ نام property باید `itemRepo` باشد وگرنه `TenantLookupInventoryTest` آن را **نمی‌شمارد** و از تور ایمنی فرار می‌کند | | ۱.۱۵ | `allowInactive` فقط برای سرویس‌های موجود نوبت، نه uuid های تازه | ✅ | پرچم روی `calculate()`؛ غیرفعال‌بودن → `warnings[]` به‌جای ۴۲۲. مدتِ نامعتبر همچنان حتی با `allowInactive` رد می‌شود (تست دارد) | | ۱.۱۶ | کنترلر نازک ماند — منطق در سرویس | ✅ | `serviceSlots()` از ۶۰ خط به ۴۰ خط؛ هیچ حلقهٔ محاسباتی در کنترلر نماند | ## ۲. دیتابیس و مهاجرت | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۲.۱ | `service_total_minutes` و `service_buffer_minutes` (تهی‌پذیر) | ✅ | `SMALLINT DEFAULT NULL` هر دو · `migrations/Version20260730091332.php` · `setServiceDuration()` هر دو را با هم ست می‌کند (بافرِ بی‌مدت معنا ندارد) | | ۲.۲ | هیچ ستون موجودی حذف/تغییر نوع/تغییر معنا نداد | ✅ | تأیید با `SHOW COLUMNS`: `slot_start` int NOT NULL · `slot_end` int NOT NULL · `active_slot_key` varchar(64) UNI · `is_reserve` tinyint default 0 — همه دست‌نخورده | | ۲.۳ | دو کد خطای جدید در `ErrorCodes.php` با پیام فارسی | ⏳ | شمارهٔ واقعی از خود فایل | | ۲.۴ | `app:appointment:backfill-service-duration` — dry-run پیش‌فرض، idempotent | ⏳ | | | ۲.۵ | backfill مقدار را از خود نوبت می‌گیرد، نه بازمحاسبه از سرویس‌ها | ⏳ | | | ۲.۶ | migration اجرا شد و `TenantSchemaCoverageTest` سبز | ✅ | dev با migration؛ **db_test با `ALTER` دستی** — تاریخچهٔ migration آن DB از dev جدا است (`Table users already exists`) و این روش نگهداشتِ موجودِ همان DB است | ## ۳. UI — پنل ادمین | # | مورد | وضعیت | یادداشت | |---|---|---|---| | ۳.۱ | `AppointmentEditPage`: حالت سرویسی `ServiceSlotPicker` نشان می‌دهد | ⏳ | | | ۳.۲ | `AppointmentEditPage`: حالت اسلاتی **دقیقاً** رفتار امروز | ⏳ | سه فیلد ساعت | | ۳.۳ | ورودی دستی ساعت در حالت سرویسی **پنهان**، نه disabled | ⏳ | | | ۳.۴ | `ServiceSlotPicker` موجود بازاستفاده شد؛ نسخهٔ موازی ساخته نشد | ⏳ | فقط prop `excludeAppointmentUuid` | | ۳.۵ | `ReserveAppointmentsPage`: سرویس‌ها + دکمهٔ تبدیل | ⏳ | | | ۳.۶ | `ReserveAppointmentsPage` از جدول خام به `DataTable` مهاجرت کرد | ⏳ | `` حذف شد | | ۳.۷ | مدت و بافر فارسی با واحد: «۳۵ دقیقه (+۱۰ دقیقه فاصله)» | ⏳ | | | ۳.۸ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن‌های `styles.css` | ⏳ | | | ۳.۹ | دارک‌مود (`data-theme="dark"`) بررسی شد | ⏳ | | | ۳.۱۰ | حالت فشرده (`data-density="compact"`) بررسی شد | ⏳ | | | ۳.۱۱ | انتخاب چند سرویس با `SearchableSelect`؛ هیچ `