"Allowed duration of a service combination" lived inside AppointmentController::serviceSlots(). Three upcoming callers need the same computation (PATCH duration validation, service-aware reschedule, reserve conversion); copying it would mean four variants with four different edge-case behaviours. The extraction is behaviour-preserving: BaseController::error() and ExceptionSubscriber emit an identical envelope, so returning $this->error() was replaced by throwing AppException with the same code/message/field. Tenant ownership now goes through TenantOwnershipChecker::belongsToPair() (the documented single point) instead of an inline section pair comparison. The repo property is named itemRepo on purpose: TenantLookupInventoryTest only counts recognised property names, so any other name would slip past the safety net. The naive duration sum is kept deliberately — switching to solo/additional minutes is task 04 and changes one line here. 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>
12 KiB
12 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 ویرایش نشد |
⏳ | قابلیت ۴ — فقط پارامتر اختیاری excludeAppointmentId روی getServiceStartTimes؛ 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 |
| ۱.۱۰ | Appointment::replaceServiceItems() + currentServiceUuids() |
⏳ | |
| ۱.۱۱ | replaceServiceItems مقدار serviceItem تکی را همگام میکند |
⏳ | چهار مصرفکننده رویش خواندهاند |
| ۱.۱۲ | setIsReserve() صدا زدن 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 (تهیپذیر) |
⏳ | |
| ۲.۲ | هیچ ستون موجودی حذف/تغییر نوع/تغییر معنا نداد | ⏳ | |
| ۲.۳ | دو کد خطای جدید در ErrorCodes.php با پیام فارسی |
⏳ | شمارهٔ واقعی از خود فایل |
| ۲.۴ | app:appointment:backfill-service-duration — dry-run پیشفرض، idempotent |
⏳ | |
| ۲.۵ | backfill مقدار را از خود نوبت میگیرد، نه بازمحاسبه از سرویسها | ⏳ | |
| ۲.۶ | migration اجرا شد و TenantSchemaCoverageTest سبز |
⏳ |
۳. 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 — سه سنجه |
⏳ | |
| ۴.۲ | ServiceBookingCalculatorTest — موفق/خطا/مرزی |
✅ | ۱۳ تست / ۲۹ assertion سبز. شامل: جمع مدت + بافر · endFor بدون بافر · override منشی بدون تغییر پیشفرض سرویس · چهار مسیر خطا با کد/پیام/فیلد دقیق · uuid ناموجود از سرویسِ محیط دیگر قابل تفکیک نیست · allowInactive → warning · فهرست خالی → صفر · override نامعتبر (۰ و منفی) → fallback · پزشک بیبرنامه → پیشفرض slot |
| ۴.۳ | ServiceRescheduleTest — شامل «حذف سرویس → مدت خودکار» |
⏳ | |
| ۴.۴ | PatchServiceDurationTest — شامل «در حالت اسلاتی هیچکدام اجرا نمیشود» |
⏳ | |
| ۴.۵ | ConvertReserveTest — شامل active_slot_key و رقابت |
⏳ | |
| ۴.۶ | ServiceModeSectionDurationTest موجود سبز ماند |
⏳ | |
| ۴.۷ | BookingTenantTest موجود سبز ماند |
⏳ | |
| ۴.۸ | 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 . |
⏳ | |
| ۶.۱۲ | موارد بهتعویقافتاده با دلیل و تسک مقصد ثبت شدند | ⏳ |