# 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 # ۱۶ تست ```