# چک‌لیست — تسک ۰۰ (تکمیل نوبت‌دهی سرویسی در 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` مهاجرت کرد | ⏳ | `<td style={td}>` حذف شد |
| ۳.۷ | مدت و بافر فارسی با واحد: «۳۵ دقیقه (+۱۰ دقیقه فاصله)» | ⏳ | |
| ۳.۸ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن‌های `styles.css` | ⏳ | |
| ۳.۹ | دارک‌مود (`data-theme="dark"`) بررسی شد | ⏳ | |
| ۳.۱۰ | حالت فشرده (`data-density="compact"`) بررسی شد | ⏳ | |
| ۳.۱۱ | انتخاب چند سرویس با `SearchableSelect`؛ هیچ `<select>` بومی | ⏳ | |
| ۳.۱۲ | `backTo`/`BackButton` روی هر دو صفحه | ⏳ | |
| ۳.۱۳ | وضعیت لیست رزروها در URL با `useUrlState` | ⏳ | |
| ۳.۱۴ | تاریخ با `PersianDateInput` · مبلغ با `formatRial` | ⏳ | |
| ۳.۱۵ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ⏳ | |
| ۳.۱۶ | موبایل بررسی شد — بدون اسکرول افقی | ⏳ | |
| ۳.۱۷ | همهٔ رشته‌ها فارسی و از i18n | ⏳ | |
| ۳.۱۸ | داده با TanStack Query و استخراج envelope درست | ⏳ | |

## ۴. تست

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `SlotModeFrozenTest` — سه سنجه | ✅ | `OK (3 tests, 8 assertions)` |
| ۴.۲ | `ServiceBookingCalculatorTest` — موفق/خطا/مرزی | ✅ | ۱۳ تست / ۲۹ assertion سبز. شامل: جمع مدت + بافر · `endFor` بدون بافر · override منشی بدون تغییر پیش‌فرض سرویس · چهار مسیر خطا با کد/پیام/فیلد دقیق · uuid ناموجود از سرویسِ محیط دیگر **قابل تفکیک نیست** · `allowInactive` → warning · فهرست خالی → صفر · override نامعتبر (۰ و منفی) → fallback · پزشک بی‌برنامه → پیش‌فرض `slot` |
| ۴.۳ | `ServiceRescheduleTest` — شامل «حذف سرویس → مدت خودکار» | ⏳ | |
| ۴.۴ | `PatchServiceDurationTest` — شامل «در حالت اسلاتی هیچ‌کدام اجرا نمی‌شود» | ⏳ | |
| ۴.۵ | `ConvertReserveTest` — شامل `active_slot_key` و رقابت | ⏳ | |
| ۴.۶ | `ServiceModeSectionDurationTest` موجود سبز ماند | ⏳ | |
| ۴.۷ | `BookingTenantTest` موجود سبز ماند | ✅ | داخل `tests/Appointment` — کل ۳۰۹ تست `tests/Appointment` + `tests/Shared` سبز |
| ۴.۱۰ | `ServiceSlotExcludeSelfTest` — رفتار exclude | ✅ | ۶ تست / ۱۱ assertion. شامل: بازهٔ خودِ نوبت با exclude برمی‌گردد · مدت بلندتر روی همان ساعت · نوبتِ دیگری همچنان اشغال می‌ماند · `null` صریح و ضمنی خروجی یکسان · فیلتر repository فقط همان ردیف · exclude کردن نوبت رزرو بی‌اثر |
| ۴.۹ | `AppointmentServiceFieldsTest` — متدها و ستون‌های جدید | ✅ | ۹ تست / ۲۴ assertion. شامل: هم‌گامی ستون تکی · حفظ ترتیب · فهرست خالی → `null` · حالت اسلاتی هر دو ستون `null` · مدتِ `null` بافر را هم `null` می‌کند · تکراری‌ها dedup · نوبت قدیمیِ فقط-تکی · بقای مقادیر پس از flush/clear |
| ۴.۸ | `AppointmentEditPage.test.tsx` — دو حالت | ⏳ | |

## ۵. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/appointment.md` — دو endpoint جدید + توسعهٔ PATCH | ⏳ | |
| ۵.۲ | ماتریس «کدام endpoint در کدام حالت» | ⏳ | |
| ۵.۳ | `docs/architecture/booking-modes.md` ساخته شد | ⏳ | تسک ۰۶ حالت سوم را اضافه می‌کند |
| ۵.۴ | دو کد خطای جدید مستند شد | ⏳ | |

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

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | همهٔ ردیف‌های بالا وضعیت نهایی دارند (هیچ 🔄 و ⏳ بی‌دلیل) | ⏳ | |
| ۶.۲ | `ddev exec php bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `ddev exec php bin/phpunit --group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan analyse` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` بدون خطا | ⏳ | |
| ۶.۶ | `yarn test` سبز | ⏳ | |
| ۶.۷ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
| ۶.۸ | `docs/api/*` به‌روز شد | ⏳ | |
| ۶.۹ | چک‌لیست UI (بخش ۳) کامل شد | ⏳ | |
| ۶.۱۰ | `nobat724_front` و `clinic-pro-tauri` دستی بررسی شدند | ⏳ | `service_item` تکی هم‌گام است؟ |
| ۶.۱۱ | commit شد، سپس `graphify update .` | ⏳ | |
| ۶.۱۲ | موارد به‌تعویق‌افتاده با دلیل و تسک مقصد ثبت شدند | ⏳ | |
