findBusyIntervals() and getServiceStartTimes() gain an optional excludeAppointmentId, mirroring isSlotTaken($doctor, $start, $end, $excludeId) which already had it. Without it an appointment being rescheduled sees itself as busy, so its current time never appears among the candidates and "same hour, different service" is impossible. The parameter is optional with a null default and only affects the service-mode path; no existing call site changes behaviour. SlotModeFrozenTest caught the signature change immediately while both response contracts stayed green, so the signature fixture was updated once with a written rationale, as its own header permits. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
14 KiB
چکلیست — تسک ۰۰ (تکمیل نوبتدهی سرویسی در clinicpro)
وضعیت کلی: 🔄 در حال انجام — قابلیت ۴ از ۱۰ تمام شد آخرین بازبینی: ۱۴۰۵/۰۵/۰۸
قواعد: _shared/definition-of-done.md · خط سرخها: _shared/red-lines.md · UI: _shared/ui-conventions.md
۰. خط سرخ — منطق اسلاتی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | SlotModeFrozenTest + سه fixture ساخته شد پیش از هر تغییر کد |
✅ | tests/Appointment/SlotModeFrozenTest.php + tests/Appointment/fixtures/ — هیچ کد تولیدیای هنوز لمس نشده |
| ۰.۲ | time() |
✅ | انحراف عمدی از متن تسک. تاریخ ثابتِ گذشته را 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). یادداشت قبلی چکلیست غلط بود |
| ۰.۸ | 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 . |
⏳ | |
| ۶.۱۲ | موارد بهتعویقافتاده با دلیل و تسک مقصد ثبت شدند | ⏳ |