# Waitlist — لیست انتظار

اندپوینت‌های `src/Waitlist/*`. «اگر وقتی در این بازه آزاد شد، خبرم کن» — توسعهٔ همان
ایدهٔ `Appointment.is_reserve` موجود، ولی با بازهٔ صریح و وضعیت.

---

## چرا broadcast و نه صف انحصاری

ظرفیت آزادشده به **حداکثر ده نفر** خبر داده می‌شود و اولین رزروکننده می‌برد.

صف انحصاری («فقط نفر اول ۳۰ دقیقه فرصت دارد») روی کاغذ عادلانه‌تر است، ولی در عمل یعنی
وقتی که کسی جوابش را نمی‌دهد نیم ساعت قفل بماند و بعد به نفر دوم برسد — و ظرفیتی که دو
ساعت مانده به نوبت آزاد شده، نیم ساعت وقتِ تلف‌کردنی ندارد.

در عوض متن پیامک **اجباراً** این را می‌گوید:

> «یک وقت برای «X» در تاریخ Y آزاد شد. اولین نفری که رزرو کند آن را می‌گیرد.»

هر درخواست حداکثر **سه بار** خبر می‌گیرد؛ بدون سقف، یک بازهٔ پرلغو به منبع اسپم تبدیل
می‌شود.

---

## GET `/api/v1/waitlist`

| Query | Type | Description |
|---|---|---|
| `status` | string | `waiting` \| `notified` \| `converted` \| `expired` |

```json
{
  "success": true,
  "data": [
    {
      "uuid": "…",
      "patient_uuid": "…",
      "service_uuid": "…",
      "service_name": "لیزر",
      "branch_id": 12,
      "desired_from": 1785600000,
      "desired_to": 1785859200,
      "preferred_day_parts": ["evening"],
      "priority": 0,
      "status": "waiting",
      "notified_at": null,
      "notify_count": 0,
      "created_at": 1785486000
    }
  ]
}
```

## POST `/api/v1/waitlist`

| Field | Type | Required | Description |
|---|---|---|---|
| `patient_uuid` | string | ✅ | |
| `service_uuid` | string | ✅ | |
| `desired_from` / `desired_to` | int | ✅ | Unix؛ بازه باید در آینده و حداکثر **۹۰ روز** باشد |
| `branch_uuid` | string | — | نبودنش یعنی «هر شعبه» |
| `preferred_day_parts` | string[] | — | فهرست **بسته**: `morning` · `afternoon` · `evening` |
| `priority` | int | — | بزرگ‌تر زودتر خبر می‌شود |

### Errors
| Code | HTTP | Description |
|---|---|---|
| `ERR_VALIDATION_001` | 422 | بازهٔ گذشته · پایانِ قبل از شروع · بازهٔ بیش از ۹۰ روز · بخش روز ناشناخته |
| `ERR_VALIDATION_002` | 422 | فیلد الزامی غایب |
| `ERR_NOT_FOUND_001` | 404 | بیمار یا سرویس خارج از محیط جاری |

## DELETE `/api/v1/waitlist/{uuid}`

## GET `/api/v1/waitlist/matches`

| Query | Type | Required |
|---|---|---|
| `service_uuid` | string | ✅ |
| `start` | int | ✅ |
| `branch_uuid` | string | — |

چه کسانی منتظر این سرویس در این لحظه‌اند؟ مرتب بر اساس اولویت، بعد قدمت. درخواستی که
شعبهٔ دیگری خواسته در نتیجه نمی‌آید؛ درخواست بی‌شعبه همیشه می‌آید.

---

## بخش روز — کجا معنا می‌شود

مرزها در `WaitlistEntry::DAY_PARTS` است و **در تطبیق اعمال می‌شود**، نه فقط ذخیره:

| کلید | برچسب | ساعت محلی |
|---|---|---|
| `morning` | صبح | `[۶, ۱۲)` |
| `afternoon` | بعدازظهر | `[۱۲, ۱۷)` |
| `evening` | عصر | `[۱۷, ۲۲)` |

ساعت به وقت **محلی شعبه** (`DoctorAddress::getTimezone()`) حساب می‌شود؛ بیمار «عصر» را
با ساعت خودش می‌فهمد نه با UTC. شعبهٔ نامشخص به `Asia/Tehran` برمی‌گردد.

فیلتر **پیش از** بریدن به ده نفر اجرا می‌شود — وگرنه ده جای اول را کسانی پر می‌کنند که
این ساعت را نمی‌خواستند و نفر یازدهمِ واقعی خبر نمی‌شود. نداشتن ترجیح یعنی «هر ساعتی».

## چرخهٔ عمر ردیف

| گذار | کِی |
|---|---|
| `waiting → notified` | اطلاع‌رسانی هنگام آزاد شدن ظرفیت (سقف ۳ بار) |
| `* → converted` | همان بیمار **همان سرویس** را در بازهٔ خواسته‌شده رزرو کرد |
| `* → expired` | `desired_to` گذشت |

`converted` از رویداد `AppointmentBooked` می‌آید، نه از داخل `BookingService`: تبدیل اثر
جانبیِ رزرو است و اگر داخل تراکنش رزرو می‌نشست، یک خطای لیست انتظار می‌توانست نوبت واقعی
بیمار را برگرداند. تطبیق عمداً تنگ است (همان بیمار + همان سرویس + زمان داخل بازه)؛ تطبیق
شل، ردیفی را می‌بندد که برای خدمت دیگری بود و بیمار دیگر هرگز خبر نمی‌شود. **idempotent**
است، پس تحویل دوبارهٔ پیام بی‌خطر است.

انقضا هر روز با زمان‌بند (`ExpireWaitlistMessage`) یا دستی:

```bash
ddev exec php bin/console app:waitlist:expire
```

ردیف منقضی از قبل هم در تطبیق نمی‌آمد (`desiredTo >= now`)، پس این پاکسازیِ **نمایش**
است: بدون آن صفحهٔ لیست انتظار پر می‌شود از انتظارهای مرده. حذف نمی‌کند، وضعیت را عوض
می‌کند — چه کسی منتظر ماند و به نتیجه نرسید، خودش داده است.

## اطلاع خودکار هنگام لغو

`POST /api/v1/appointment/{uuid}/cancel` بعد از آزادسازی ظرفیت، لیست انتظار را خبر
می‌کند و تعدادش را در `waitlist_notified` برمی‌گرداند. جزئیات لغو:
[cancellation.md](cancellation.md)

## تست‌ها

```bash
ddev exec php bin/phpunit tests/Waitlist   # ۱۶ تست
```
