Files
clinicpro/docs/architecture/booking-modes.md
T
hamedandClaude Opus 5 9891c2e44a docs(booking): document service booking mode and close task 00
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>
2026-07-30 15:27:18 +03:30

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 را اضافه می‌کند: برنامهٔ چندبخشی روی چند منبع (اتاق، دستگاه، اپراتور). ارتقا یک‌طرفه خواهد بود و مشروط بر نبودِ نوبت فعال آینده. تا آن زمان، این سند دو حالت دارد و همین ماتریس معتبر است.