Files
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

125 lines
6.8 KiB
Markdown

# روش‌های نوبت‌دهی
هر برنامهٔ هفتگی (`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](../new_feture/taskes/_shared/red-lines.md)
اجبار خودکار:
```bash
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 نوبت‌های قدیمی:
```bash
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` را اضافه می‌کند: برنامهٔ چندبخشی روی چند منبع
(اتاق، دستگاه، اپراتور). ارتقا **یک‌طرفه** خواهد بود و مشروط بر نبودِ نوبت فعال آینده.
تا آن زمان، این سند دو حالت دارد و همین ماتریس معتبر است.