docs/api/appointment.md gains the service-reschedule endpoint, the service-mode section under PATCH, exclude_appointment_uuid and clinic_uuid on appointment-service-slots, and the my/appointments additions. All JSON bodies are real output captured from the running endpoints, not hand-written. New docs/architecture/booking-modes.md holds the endpoint/mode matrix, the duration contract with a worked example (35 + 10 buffer means a 45-minute step, so 11:00 is not offered even though it looks free), the reserve-entry rules, and a placeholder for the resource mode task 06 will add. Also fixes a pre-existing flaky test that blocked a green suite: NumericFieldNormalizerTest used a fixed national_code against db_test, which is never reset, so depending on execution order the endpoint rejected it as a duplicate. The test already looped for a unique mobile but not for the national code. Out of this task's scope, fixed and declared so the definition of done is actually green rather than apparently green. phpstan was measured against the pre-task commit rather than asserted: 14 errors in 9 files before, the same 14 in the same 9 files now. Task 00 complete: 1026 tests green across three consecutive runs, 604 frontend tests green, slot-mode contract frozen and verified. 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>
6.8 KiB
روشهای نوبتدهی
هر برنامهٔ هفتگی (weekly_schedules) یک روش نوبتدهی دارد که در
setting.meta.booking_mode مینشیند. روش، per محل است نه per پزشک: یک پزشک میتواند
در مطب شخصی اسلاتی و در کلینیک سرویسی باشد، چون هر محل برنامهٔ مستقل خودش را دارد
(UNIQUE(doctor_id, entity_type, entity_id)).
| روش | مقدار | مدت نوبت از کجا میآید |
|---|---|---|
| اسلاتی | slot (پیشفرض) |
duration_per_patient هر شیفت — اسلاتهای هماندازه |
| سرویسی | service |
مجموع مدت سرویسهای انتخابی + buffer_minutes بین دو نوبت |
booking_mode پس از اولین ثبت قفل میشود
({@see \App\Appointment\Entity\WeeklySchedule::getStoredBookingMode()}) و پنل توگل را
غیرفعال میکند. تا وقتی null است قابل تغییر است.
⛔ حالت اسلاتی منجمد است
منطق slot تولیدی و زنده است. در فاز موتور نوبتدهی چندمنبعی هیچ تسکی اجازهٔ تغییرش را
ندارد. فهرست کامل قفلشدهها:
docs/new_feture/taskes/_shared/red-lines.md
اجبار خودکار:
ddev exec php bin/phpunit --group=slot-mode-frozen
سه چیز را قفل میکند: شکل پاسخ appointment-slots، شکل پاسخ month-availability، و
امضای متدهای عمومی SlotCalculatorService. fixture ها در
tests/Appointment/fixtures/ read-only اند — اگر تست قرمز شد، کد باید برگردد نه fixture.
ماتریس: کدام endpoint در کدام حالت
| endpoint | slot |
service |
|---|---|---|
GET /api/v1/appointment-slots |
✅ | — |
GET /api/v1/appointment-settings/month-availability/{doctorUuid} |
✅ | ✅ (فقط «این روز ظرفیت دارد؟») |
GET /api/v1/appointment-booking-services/{doctorUuid} |
✅ (booking_mode را میگوید) |
✅ |
GET /api/v1/appointment-service-slots |
❌ ۴۲۲ | ✅ |
GET /api/v1/appointment-booking-locations/{doctorUuid} |
✅ | ✅ |
POST /api/v1/appointment |
✅ | ✅ |
PATCH /api/v1/appointment/{uuid} |
✅ (هر مدتی) | ✅ (مدت اعتبارسنجی میشود) |
POST /api/v1/appointment/{uuid}/service-reschedule |
❌ ۴۲۲ ERR_APPOINTMENT_004 |
✅ |
appointment-service-slots تنها endpointی است که حالت را صریحاً رد میکند
(این پزشک در حالت نوبتدهی سرویسی نیست). بقیه یا هر دو حالت را میپذیرند یا بیاثرند.
حالت سرویسی — قرارداد مدت
مدت نوبت = مجموع مدت سرویسهای انتخابی (یا override منشی per سرویس)
پایان نوبت = شروع + مدت ← بافر جزو نوبت نیست
گام کاندیدها = مدت + بافر ← فاصلهٔ بین دو نوبت
مثال واقعی (شیفت ۰۹:۰۰–۱۲:۰۰، سرویس ۲۰+۱۵ دقیقه، بافر ۱۰):
۰۹:۰۰–۰۹:۳۵ ۰۹:۴۵–۱۰:۲۰ ۱۰:۳۰–۱۱:۰۵ ۱۱:۱۵–۱۱:۵۰
گام ۴۵ دقیقه است، پس ۱۱:۰۰ پیشنهاد نمیشود حتی اگر آزاد به نظر برسد. هر مسیری که
زمان میگیرد باید عضویت در همین فهرست را بسنجد، نه فقط «اشغال نیست»:
isSlotTaken() تنها تداخل با نوبت دیگر را میگوید، ولی فهرست پیشنهادی شیفت، تعطیلی،
date_override، پنجرهٔ رزرو و بافر را هم اعمال میکند.
تنها مرجع محاسبه
App\Appointment\Service\ServiceBookingCalculator — مالکیت محیط، وجود، فعالبودن و مدت،
همه یکجا. سه مصرفکننده دارد: appointment-service-slots، اعتبارسنجی PATCH، و
ServiceRescheduleService.
⚠️ فرمول فعلی جمع سادهٔ مدتهاست. مستند موتور نوبتدهی (بند ۵) این را رد میکند — آمادهسازی چند بار حساب میشود و ظرفیت الکی پر میشود. اصلاحش («زمان تنها / زمان اضافه») کار تسک ۰۴ است و همانجا یک خط از این کلاس عوض میشود.
ستونهای ثبتشده روی نوبت
| ستون | معنا |
|---|---|
service_total_minutes |
مدت محاسبهشده در لحظهٔ ثبت. در حالت اسلاتی NULL |
service_buffer_minutes |
بافر مؤثر در لحظهٔ ثبت — تغییر تنظیمات نوبتهای ثبتشده را عوض نمیکند |
slot_end - slot_start همان عدد را دارد ولی نمیگوید عمدی بود یا دستی؛ و نوبت رزرو
(slot_start == slot_end) هیچ جای دیگری مدت نگه نمیدارد.
backfill نوبتهای قدیمی:
ddev exec php bin/console app:appointment:backfill-service-duration # dry-run
ddev exec php bin/console app:appointment:backfill-service-duration --force
نوبت رزرو (is_reserve)
نوبت رزرو روزی است نه ساعتی: slot_start == slot_end == نیمهشب، active_slot_key
تهی، و هیچ بازهای اشغال نمیکند.
| عملیات | رفتار |
|---|---|
| ثبت سرویسها روی رزرو | ✅ سرویسها و service_total_minutes ذخیره میشوند تا تبدیل بعدی مدت را از دست ندهد |
| اعتبارسنجی مدت | معاف — مدتی برای سنجیدن ندارد |
service-reschedule |
❌ ۴۲۲ — رزرو زمان ندارد |
| تبدیل به نوبت زماندار | PATCH با is_reserve: false + slot_start/slot_end |
endpoint جدایی برای تبدیل وجود ندارد و لازم نیست: rescheduleTo($start, $end, $isReserve)
خودش refreshActiveSlotKey() را صدا میزند و کلید یکتایی را بازتولید میکند.
حالت سوم (resource) — هنوز نیامده
تسک ۰۶ فاز موتور چندمنبعی حالت resource را اضافه میکند: برنامهٔ چندبخشی روی چند منبع
(اتاق، دستگاه، اپراتور). ارتقا یکطرفه خواهد بود و مشروط بر نبودِ نوبت فعال آینده.
تا آن زمان، این سند دو حالت دارد و همین ماتریس معتبر است.