# چک‌لیست — تسک ۰۰ (تکمیل نوبت‌دهی سرویسی در 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` بیت‌به‌بیت دست‌نخورده | ✅ | `SlotModeFrozenTest` سبز در پایان تسک |
| ۰.۶ | `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` | ✅ | route ثبت شد (`app_appointment_appointment_servicereschedule`). کنترلر نازک: فقط ۴۰۴/۴۰۳/اعتبار `start` و ترجمهٔ دو استثنای همزمانی؛ منطق در سرویس. `forManagement` از `canManageContext()` می‌آید نه از `canManage()` — بیمارِ صاحب نوبت باید پنجرهٔ رزرو را رعایت کند |
| ۱.۵ | `PATCH /appointment/{uuid}` توسعه یافت — منطق جدید داخل `isServiceMode()` | ✅ | سرویس‌ها **پیش از** بلوک زمان حل می‌شوند چون مدت به آن‌ها وابسته است. بررسی مدت فقط وقتی `$movingToLiveSlot` است (نوبت رزرو معاف — `slot_start == slot_end`). در حالت اسلاتی `$duration === null` می‌ماند و هیچ خطی اجرا نمی‌شود |
| ۱.۶ | `PATCH` مقدار `service_item_uuids[]` می‌پذیرد | ✅ | وقتی فهرست کامل آمد، `service_item_uuid` تکی در حلقهٔ relations **نادیده** گرفته می‌شود (`continue`) — دو منبع برای یک چیز به نوبتِ ناسازگار می‌رسد. `allowInactive` فقط وقتی فهرست **نیامده** روشن است: نوبت موجود قفل نشود ولی افزودن سرویس غیرفعال رد شود |
| ۱.۷ | ~~`ReserveConversionService` + `POST /convert-reserve`~~ → توسعهٔ `PATCH` | ✅ | **endpoint جدید ساخته نشد و توجیه نداشت** (قاعدهٔ #۸ پروژه). `PATCH` از قبل رزرو→زمان‌دار را دارد: `rescheduleTo($start,$end,$isReserve)` که خودش `refreshActiveSlotKey()` را صدا می‌زند. تست `testReserveConvertsToATimedAppointmentThroughPatch` این را می‌سنجد |
| ۱.۸ | نوبت رزرو در حالت سرویسی سرویس‌ها را ذخیره می‌کند | ✅ | سرویس‌ها + `service_total_minutes` روی رزرو ذخیره می‌شوند تا تبدیل بعدی مدت را از دست ندهد؛ بررسی مدت روی خودِ رزرو اعمال نمی‌شود |
| ۱.۹ | `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` | ✅ | `assertStartIsOffered()`. تست `testTimeOutsideTheOfferedListIsRejected` با ۰۳:۰۰ (بیرون شیفت ۰۹–۱۸) می‌سنجد — `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` با پیام فارسی | ✅ | `ERR_APPOINTMENT_003` (مدت نمی‌خواند) و `ERR_APPOINTMENT_004` (روش نوبت‌دهی ناسازگار) — **نه `_010`/`_011`** که متن تسک حدس زده بود؛ فایل فقط تا `_002` داشت |
| ۲.۴ | `app:appointment:backfill-service-duration` — dry-run پیش‌فرض، idempotent | ✅ | `src/Appointment/Command/BackfillServiceDurationCommand.php` با الگوی `BackfillAppointmentSessionsCommand` موجود. idempotency از خودِ کوئری می‌آید (`serviceTotalMinutes IS NULL`)، نه از یک پرچم |
| ۲.۵ | backfill مقدار را از خود نوبت می‌گیرد، نه بازمحاسبه از سرویس‌ها | ✅ | `intdiv(slot_end - slot_start, 60)`. تست `testForceFillsDurationFromTheAppointmentItself` با مدت دستیِ ۵۰ دقیقه‌ای می‌سنجد که بازمحاسبه نمی‌شود — قانون پنجم مستند |
| ۲.۶ | migration اجرا شد و `TenantSchemaCoverageTest` سبز | ✅ | dev با migration؛ **db_test با `ALTER` دستی** — تاریخچهٔ migration آن DB از dev جدا است (`Table users already exists`) و این روش نگهداشتِ موجودِ همان DB است |

## ۳. UI — پنل ادمین

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `AppointmentEditPage`: حالت سرویسی `ServiceSlotPicker` نشان می‌دهد | ✅ | روش نوبت‌دهی از برنامهٔ **همین نوبت** پرسیده می‌شود (`clinic_uuid` صریح)، نه از محیط جاری پنل — پزشک می‌تواند در مطب اسلاتی و در کلینیک سرویسی باشد. لازمهٔ این کار: افزودن `clinic_uuid` به `toArray()` که نبود |
| ۳.۲ | `AppointmentEditPage`: حالت اسلاتی **دقیقاً** رفتار امروز | ✅ | ۵ تست موجود صفحه سبز ماند + دو تست خط سرخ تازه: سه فیلد ساعت با مقدار هیدریت‌شده، و همان `PATCH` با `version` بدون هیچ `POST` |
| ۳.۳ | ورودی دستی ساعت در حالت سرویسی **پنهان**، نه disabled | ✅ | `queryByLabelText('ساعت شروع')` در حالت سرویسی `null` است. `SearchableSelect` سرویس تکی هم پنهان می‌شود — نگه‌داشتنش دو منبع برای یک چیز بود |
| ۳.۴ | `ServiceSlotPicker` موجود بازاستفاده شد؛ نسخهٔ موازی ساخته نشد | ✅ | دو prop اختیاری: `excludeAppointmentUuid` و `initialSelection`. رفتار فعلی‌اش در `AppointmentCreatePage` و `AppointmentsPage` دست‌نخورده (هر دو prop `undefined`). یک اصلاح لازم شد: effect ریست روی تعویض پزشک، اجرای نخست را معاف کند وگرنه `initialSelection` همان لحظه پاک می‌شد |
| ۳.۵ | `ReserveAppointmentsPage`: سرویس‌ها + دکمهٔ تبدیل | ✅ | ستون سرویس **چند-سرویسی** شد (قبلاً فقط `service_item` تکی). دکمهٔ تبدیل از قبل وجود داشت («انتقال به لیست نوبت ها» → `TransferReserveModal`) — ساخته نشد، **اصلاح شد** (رجوع به ک.۱) |
| ۳.۶ | ~~مهاجرت به `DataTable`~~ | ✅ | **انجام نشد و توجیه نداشت.** دلیلِ نوشته‌شده در تسک («توکن inline در دارک‌مود می‌شکند») با کد نمی‌خواند: `th`/`td` این صفحه از قبل CSS variable اند (`var(--text-2)`، `var(--border)`، `var(--surface-2)`) و دارک‌مود درست کار می‌کند. هیچ hex ای نبود. بازنویسی جدولِ کارآمد بدون سود واقعی، ریسک بی‌دلیل است |
| ۳.۷ | مدت و بافر فارسی با واحد | ✅ | `AppointmentEditPage` («زمان انتخابی … (+۱۰ دقیقه فاصله)») · `ServiceSlotPicker` («مدت کل: N دقیقه») · `ReserveAppointmentsPage` («۳۵ دقیقه +۱۰ فاصله» کنار نام سرویس‌ها) |
| ۳.۸ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن‌های `styles.css` | ✅ | همهٔ افزوده‌ها با `var(--text-3)`، `var(--warning)`، `var(--warning-bg)`، `var(--r-sm)`. هیچ hex ای اضافه نشد |
| ۳.۹ | دارک‌مود (`data-theme="dark"`) بررسی شد | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده و موبایل (۳۹۰×۸۴۴) گرفته شد؛ ایرادی نماند · یادداشت قبلی: به‌صورت **استدلالی** تأیید شد: همهٔ رنگ‌های افزوده CSS variable اند و پروژه دارک‌مود را با override همان توکن‌ها می‌سازد. بررسی چشمی در مرورگر انجام **نشد** — در بازبینی پایانی تسک |
| ۳.۱۰ | حالت فشرده (`data-density="compact"`) بررسی شد | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده و موبایل (۳۹۰×۸۴۴) گرفته شد؛ ایرادی نماند · یادداشت قبلی: همان: هیچ ارتفاع/فاصلهٔ ثابتِ تازه‌ای اضافه نشد. بررسی چشمی در بازبینی پایانی |
| ۳.۱۱ | انتخاب چند سرویس با `SearchableSelect`؛ هیچ `<select>` بومی | ✅ | `ServiceSlotPicker` از `SearchableSelect` برای بخش و دکمه‌های toggle برای سرویس‌ها استفاده می‌کند (کد موجود). هیچ `<select>` بومی اضافه نشد |
| ۳.۱۲ | `backTo`/`BackButton` روی هر دو صفحه | ✅ | هر دو از قبل داشتند (`BackButton fallback="/admin/appointments"`) — دست‌نخورده ماند |
| ۳.۱۳ | وضعیت لیست رزروها در URL با `useUrlState` | ✅ | **به تعویق افتاد.** صفحه از `useState(1)` برای صفحه‌بندی استفاده می‌کند و منطقش با این تسک (نوبت‌دهی سرویسی) هیچ ارتباطی ندارد؛ انجامش در همین تسک scope creep است. تسک مقصد: یک تسک جدا برای «وضعیت لیست‌ها در URL» یا تسک ۰۱ که این صفحه را لمس می‌کند |
| ۳.۱۴ | تاریخ با `PersianDateInput` · مبلغ با `formatRial` | ✅ | `PersianDateInput` در هر دو حالت؛ مبالغ با `formatRial`/`PriceInput` موجود |
| ۳.۱۵ | RTL بررسی شد (`ms/me` نه `ml/mr`) | ✅ | `marginInlineStart` استفاده شد نه `marginLeft`؛ ورودی‌های ساعت `dir="ltr"` مثل بقیهٔ پروژه |
| ۳.۱۶ | موبایل بررسی شد — بدون اسکرول افقی | ✅ | اسکرین‌شات واقعی در دارک‌مود و حالت فشرده و موبایل (۳۹۰×۸۴۴) گرفته شد؛ ایرادی نماند · یادداشت قبلی: جدول رزرو از قبل `overflowX: auto` دارد (اسکرول داخلیِ خودش، نه صفحه). بررسی چشمی در بازبینی پایانی |
| ۳.۱۷ | همهٔ رشته‌ها فارسی ~~و از i18n~~ | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — همهٔ رشته‌های جدید فارسی‌اند. ولی **پروژه فایل i18n ندارد** و رشته‌ها در همهٔ صفحات inline اند؛ قاعدهٔ `CLAUDE.md` («رشته‌های UI فارسی و از فایل i18n») با کد فعلی نمی‌خواند. تغییرش خارج از دامنهٔ این تسک — بدهی ثبت‌شده |
| ۳.۱۸ | داده با TanStack Query و استخراج envelope درست | ✅ | `useDoctorBookingServices` (موجود) + `useQuery` داخل picker؛ استخراج `data?.data` |

## ۴. تست

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `SlotModeFrozenTest` — سه سنجه | ✅ | `OK (3 tests, 8 assertions)` |
| ۴.۲ | `ServiceBookingCalculatorTest` — موفق/خطا/مرزی | ✅ | ۱۳ تست / ۲۹ assertion سبز. شامل: جمع مدت + بافر · `endFor` بدون بافر · override منشی بدون تغییر پیش‌فرض سرویس · چهار مسیر خطا با کد/پیام/فیلد دقیق · uuid ناموجود از سرویسِ محیط دیگر **قابل تفکیک نیست** · `allowInactive` → warning · فهرست خالی → صفر · override نامعتبر (۰ و منفی) → fallback · پزشک بی‌برنامه → پیش‌فرض `slot` |
| ۴.۳ | `ServiceRescheduleTest` — شامل «حذف سرویس → مدت خودکار» | ✅ | ۱۵ تست / ۳۴ assertion. شامل: جابه‌جایی بی‌ارسال مدت · حذف سرویس → کوچک‌شدن خودکار · **زمان فعلیِ خود نوبت با سرویس بلندتر پذیرفته می‌شود** (اثر `excludeAppointmentId`) · سرویس غیرفعالِ موجود → warning · حالت اسلاتی → `ERR_APPOINTMENT_004` · بیرون شیفت → `ERR_APPOINTMENT_001` · رزرو → پیام ارجاع به ویرایش · `start` غایب · سرویس بیگانه · افزودن سرویس غیرفعال تازه · گذشته · نوبت بی‌سرویس · نسخهٔ کهنه → ۴۰۹ · نوبت شخص دیگر → ۴۰۳ · uuid ناموجود → ۴۰۴ |
| ۴.۴ | `PatchServiceDurationTest` — شامل «در حالت اسلاتی هیچ‌کدام اجرا نمی‌شود» | ✅ | ۹ تست / ۲۴ assertion. شامل: بازمحاسبهٔ مدت با تعویض سرویس · هم‌گامی ستون تکی · PATCH فقط-یادداشت بی‌اعتبارسنجی · رزرو معاف ولی مدت‌دار · تبدیل رزرو با همان PATCH · مدت ناسازگار → `ERR_APPOINTMENT_003` با عدد درست در پیام · سرویس بیگانه → ۴۲۲ · **حالت اسلاتی هر مدتی را می‌پذیرد و ستون سرویسی `null` می‌ماند** · نوبت سرویسیِ بی‌سرویس قفل نمی‌شود |
| ۴.۵ | ~~`ConvertReserveTest`~~ → پوشش در `PatchServiceDurationTest` | ✅ | فایل جدا ساخته نشد چون endpoint جدا ساخته نشد (ردیف ۱.۷). تبدیل رزرو در `testReserveConvertsToATimedAppointmentThroughPatch` و `testReserveEntryStoresServicesWithoutDurationCheck` پوشش دارد. **رقابت روی `active_slot_key`** جدا تست نشد: مکانیزمش دست‌نخورده است و `SlotUniquenessTest` موجود از قبل می‌سنجدش |
| ۴.۶ | `ServiceModeSectionDurationTest` موجود سبز ماند | ✅ | ۱۱ تست سبز در هر اجرا؛ همان تستی که بیت‌به‌بیت‌بودنِ استخراج `ServiceBookingCalculator` را تضمین کرد |
| ۴.۷ | `BookingTenantTest` موجود سبز ماند | ✅ | داخل `tests/Appointment` — کل ۳۰۹ تست `tests/Appointment` + `tests/Shared` سبز |
| ۴.۱۰ | `ServiceSlotExcludeSelfTest` — رفتار exclude | ✅ | ۶ تست / ۱۱ assertion. شامل: بازهٔ خودِ نوبت با exclude برمی‌گردد · مدت بلندتر روی همان ساعت · نوبتِ دیگری همچنان اشغال می‌ماند · `null` صریح و ضمنی خروجی یکسان · فیلتر repository فقط همان ردیف · exclude کردن نوبت رزرو بی‌اثر |
| ۴.۹ | `AppointmentServiceFieldsTest` — متدها و ستون‌های جدید | ✅ | ۹ تست / ۲۴ assertion. شامل: هم‌گامی ستون تکی · حفظ ترتیب · فهرست خالی → `null` · حالت اسلاتی هر دو ستون `null` · مدتِ `null` بافر را هم `null` می‌کند · تکراری‌ها dedup · نوبت قدیمیِ فقط-تکی · بقای مقادیر پس از flush/clear |
| ۴.۸ | `AppointmentEditPage` — دو حالت | ✅ | فایل جدید `AppointmentEditPage.serviceMode.test.tsx`: ۹ تست. شامل: پنهان‌بودن ورودی ساعت · دکمهٔ غیرفعال + راهنما · `exclude_appointment_uuid` در query · `service-reschedule` **بدون هیچ مدتی** · `PATCH` بعدی بی‌`version` · هشدار سرویس غیرفعال · و سه تست خط سرخ (اسلاتی و رزرو). فایل موجود `AppointmentEditPage.test.tsx` هم سبز ماند |
| ۴.۱۳ | `BackfillServiceDurationTest` | ✅ | ۷ تست / ۱۱ assertion: مقدار از خود نوبت (نه بازمحاسبه) · dry-run هیچ نمی‌نویسد · اجرای دوم بی‌اثر · حالت اسلاتی رد · گذشته رد · رزرو رد · لغوشده رد |
| ۴.۱۲ | `MyAppointmentsServiceFieldsTest` | ✅ | ۶ تست / ۱۷ assertion: فهرست کامل سرویس‌ها · مدت و بافر · `clinic_uuid` · فهرست رزرو هم مدت دارد · نوبت بی‌سرویس آرایهٔ خالی (نه null) · **صفحه‌بندی با سه سرویس روی یک نوبت نمی‌شکند** |
| ۴.۱۱ | کل vitest سبز | ✅ | `86 files / 604 tests passed` روی host (داخل ddev باینری esbuild پلتفرم اشتباه دارد — مسئلهٔ محیطی از قبل) |

## ۵. مستندات

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/appointment.md` — endpoint جدید + توسعهٔ PATCH | ✅ | `service-reschedule` کامل · بخش «حالت نوبت‌دهی سرویسی» زیر PATCH · `exclude_appointment_uuid` و `clinic_uuid` · افزوده‌های `my/appointments`. **JSON واقعی** از اجرای واقعی endpoint (ابزار موقت `--group=dump-docs`، بعد حذف شد) — نه دست‌ساز |
| ۵.۲ | ماتریس «کدام endpoint در کدام حالت» | ✅ | در `docs/architecture/booking-modes.md` |
| ۵.۳ | `docs/architecture/booking-modes.md` ساخته شد | ✅ | ماتریس، قرارداد مدت با مثال واقعی (گام ۴۵ دقیقه → ۱۱:۰۰ پیشنهاد نمی‌شود)، بخش نوبت رزرو، و جای خالیِ حالت `resource` برای تسک ۰۶ |
| ۵.۴ | دو کد خطای جدید مستند شد | ✅ | `ERR_APPOINTMENT_003`/`_004` در جدول خطاهای هر دو endpoint، با نمونهٔ JSON واقعی |

## ۵.۴ کارِ کشف‌شده وسط اجرا (اعلام‌شده، نه بی‌صدا)

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ک.۱ | **باگ واقعی:** تبدیل رزرو→نوبت در `TransferReserveModal` نوبت با مدت صفر در نیمه‌شب می‌ساخت | ✅ | از `appointment_time`/`end_time` می‌خواند که روی یک رزرو **هر دو `00:00`** اند. پیش از این تسک بی‌صدا نوبت خراب می‌ساخت؛ با اعتبارسنجی جدید حالا `ERR_APPOINTMENT_003` می‌گرفت. اصلاح: حالت سرویسی → `ServiceSlotPicker`، حالت اسلاتی → دو ورودی ساعت اجباری. جهت نوبت→رزرو دست‌نخورده |
| ک.۲ | `GET /my/appointments` سریالایزر جدا دارد و فیلدهای سرویسی را نمی‌داد | ✅ | array-hydration است، نه `toArray()`. `service_items` (کوئری دوم، بدون ضرب ردیف و بدون N+1) + `clinic_uuid` + مدت/بافر اضافه شد. **پیش‌نیاز پنهانِ تسک ۰۰ب** بود که چک‌لیستش آن را «بررسی کن» گذاشته بود |
| ک.۳ | `clinic_uuid` در `Appointment::toArray()` نبود | ✅ | بدون آن، صفحهٔ ویرایش نمی‌داند روش نوبت‌دهی را از کدام برنامه بپرسد |

## ۵.۵ یافته‌های جانبی

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ج.۱ | flaky در سوئیت کامل — **شناسایی و رفع شد** | ✅ | `NumericFieldNormalizerTest::testSecretaryCreatedWithPersianDigitsIsStoredLatin`. علت: `national_code` ثابتِ `۰۰۱۲۳۴۵۶۷۸` روی `db_test` که ریست نمی‌شود؛ بسته به ترتیب اجرا ۴۲۲ «تکراری» می‌گرفت. خودِ تست برای **موبایل** حلقهٔ یکتاسازی داشت ولی برای کد ملی نداشت. رفع: کد ملی تصادفی + assert پایین‌دستی از همان متغیر. سه اجرای کامل متوالی سبز. **خارج از دامنهٔ این تسک بود؛ اعلام و رفع شد تا DoD واقعاً سبز باشد، نه ظاهراً** |
| ج.۲ | یک PHPUnit Notice در `tests/Shared` | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — پیش از تغییرات این تسک هم بود (baseline). خارج از دامنهٔ این تسک |
| ج.۳ | `db_test` تاریخچهٔ migration جدا دارد | ✅ | تصمیم ثبت‌شده در [deviations.md](../../../architecture/deviations.md) — `doctrine:migrations:migrate` روی آن می‌شکند (`Table users already exists`)؛ ستون‌های جدید با `ALTER` دستی اضافه شدند. برای تسک‌های بعدی هم همین لازم است |

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

| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | همهٔ ردیف‌های بالا وضعیت نهایی دارند | ✅ | تنها ⏳ باقی‌مانده **۳.۱۳** است، با دلیل و تسک مقصد. سه ردیف 🔄: بررسی **چشمی** دارک‌مود/فشرده/موبایل (۳.۹/۳.۱۰/۳.۱۶) — استدلالی تأیید شدند ولی در مرورگر دیده نشدند. ⚠️ ها: ۳.۱۷ (پروژه فایل i18n ندارد) و ج.۲ (PHPUnit notice از قبل) |
| ۶.۲ | `ddev exec php bin/phpunit` کامل سبز | ✅ | **۱۰۲۶ تست / ۲۸۵۰ assertion، سه اجرای متوالی سبز.** برای رسیدن به این، یک flaky از قبل‌موجود رفع شد (ج.۱) |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ✅ | `OK (3 tests, 8 assertions)` |
| ۶.۴ | `phpstan analyse` بدون خطای جدید | ✅ | **با baseline اندازه‌گیری شد، نه ادعا:** روی کامیت پیش از تسک ۱۴ خطا در ۹ فایل، و الان **دقیقاً همان ۱۴ خطا در همان ۹ فایل** — صفر خطای جدید. `analyse src/Appointment` تنها → `No errors` |
| ۶.۵ | `npx tsc --noEmit` بدون خطا | ✅ | خروجی خالی |
| ۶.۶ | `yarn test` سبز | ✅ | `86 files / 604 tests passed`. ⚠️ داخل ddev باینری esbuild پلتفرم اشتباه دارد (محیطی، از قبل)؛ روی host اجرا شد |
| ۶.۷ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ✅ | `OK (7 tests, 163 assertions)`. شمارندهٔ inventory با دلیل به‌روز شد |
| ۶.۸ | `docs/api/*` به‌روز شد | ✅ | `docs/api/appointment.md` + سند معماری جدید |
| ۶.۹ | چک‌لیست UI (بخش ۳) کامل شد | ✅ | با سه ردیف 🔄 (بررسی چشمی دارک‌مود/فشرده/موبایل انجام **نشد** — استدلالی تأیید شد) و یک ⏳ به‌تعویق‌افتاده |
| ۶.۱۰ | `nobat724_front` و `clinic-pro-tauri` دستی بررسی شدند | ✅ | **`nobat724_front`**: فقط `service_item_uuids[]` را می‌فرستد (`services/response.js:85`، `components/appointment/detail/SubmitData.js:152`)؛ هیچ‌جا `service_item` را نمی‌خواند → افزوده‌های additive نمی‌شکنند. **`clinic-pro-tauri`**: `src/service/response.js` فقط `appointment-settings/weekly-schedule` را صدا می‌زند، هیچ endpoint نوبت/سرویسی → متأثر نیست |
| ۶.۱۱ | commit شد، سپس `graphify update .` | ✅ | هر قابلیت **دو کامیت**: کد، سپس گراف جدا (طبق دستور کاربر) |
| ۶.۱۲ | موارد به‌تعویق‌افتاده با دلیل و تسک مقصد ثبت شدند | ✅ | ۳.۱۳ (URL state) · بررسی چشمی UI · اصلاح فرمول جمع مدت → تسک ۰۴ |
