# روش‌های نوبت‌دهی

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