feat: implement cancellation policy, no-show tracking, and waitlist management

- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
This commit is contained in:
hamed
2026-07-30 11:43:58 +03:30
parent 1d338503c8
commit 021d0eb6b2
62 changed files with 8098 additions and 0 deletions
@@ -0,0 +1,158 @@
# نکات پیاده‌سازی — تسک ۰۶
## ۱. سه کوئری، بعد هیچ
قاعدهٔ غیرقابل‌مذاکره: **داخل حلقهٔ روز و حلقهٔ نقطهٔ شروع، هیچ I/O نیست.**
```php
// ❌ مرگ کارایی
foreach ($days as $day) {
foreach ($starts as $start) {
if ($this->occupancyRepo->isFree($resource, $start, $end)) { } // ← N×M کوئری
}
}
// ✅
$index = OccupancyIndex::build($this->occupancyRepo->findForResources($ids, $from, $to), $windows);
foreach ($starts as $start) { $index->hasRoom($id, $start, $end, 1); }
```
`AvailabilityPerformanceTest` تعداد کوئری را قفل می‌کند تا اولین refactor این را نشکند.
## ۲. `setup/cleanup` — بازهٔ اشغال، نه بازهٔ بخش
```php
$occStart = $segmentStart - $resource->getSetupMinutes() * 60;
$occEnd = $segmentEnd + $resource->getCleanupMinutes() * 60;
$index->hasRoom($resourceId, $occStart, $occEnd, $units);
```
نکتهٔ ظریف: `setup/cleanup` per **منبع** است، ولی منبع در لحظهٔ بررسی هنوز انتخاب نشده.
پس دو گذر:
1. بررسی اولیه با **بیشینهٔ** `setup/cleanup` کاندیدها (محافظه‌کار)
2. بعد از انتخاب منبع، بازهٔ دقیق همان منبع محاسبه و دوباره بررسی شود
گذر دوم ارزان است (یک منبع، یک بازه) و از رد شدن اشتباه کاندیدها جلوگیری می‌کند.
## ۳. `capacity` و `units`
```
exclusive → units = capacity (منبع کامل)
shared → units = 1
passive → units = capacity (رزرو است، ولی پرچم passive برای گزارش)
```
`hasRoom` جمع `units` اشغال‌های متداخل را با `capacity` مقایسه می‌کند. با این مدل،
اتاق تزریق سه‌تخته با یک ردیف کار می‌کند و شمارش خودکار است.
## ۴. منبع مشترک بین بخش‌ها
مثال مستند: اپراتور در بخش ۱ (۰-۵) و بخش ۳ (۳۵-۵۵) لازم است، در بخش ۲ نه.
- **باید همان اپراتور باشد** → `groupKey` در `ResourceAllocator`
- **در بخش ۲ نباید اشغال بماند** → دو ردیف اشغال جدا، نه یکی از ۰ تا ۵۵
اگر ردیف را یکی کنی، کل ارزش این پروژه از بین می‌رود: همان ۳۰ دقیقه‌ای که می‌خواستیم
آزاد کنیم دوباره قفل می‌شود. تست پذیرش «آزادسازی ظرفیت» دقیقاً همین را می‌سنجد.
## ۵. مسیر قدیمی دست‌نخورده
`SlotCalculatorService` **هیچ تغییری نمی‌کند**. `AvailabilityEngine` یک کلاس جدید کنارش است.
انتخاب بین این دو فقط در کنترلر و بر اساس `booking_mode`:
```php
$mode = $schedule?->getMeta()['booking_mode'] ?? WeeklySchedule::MODE_SLOT;
return match ($mode) {
WeeklySchedule::MODE_RESOURCE => $this->availabilityEngine->search($req),
WeeklySchedule::MODE_SERVICE => $this->slotCalculator->getServiceStartTimes(), // بدون تغییر
default => $this->slotCalculator->getAvailableSlots(), // بدون تغییر
};
```
هر endpoint فقط حالت خودش را می‌پذیرد و بقیه را با `ERR_WRONG_BOOKING_MODE` رد می‌کند —
نه fallback خاموش. fallback خاموش یعنی کلینیکی که فکر می‌کند حالت جدید دارد، بی‌صدا
روی حالت قدیم کار می‌کند و هیچ‌کس نمی‌فهمد چرا ظرفیتش باز نشد.
## ۶. ارتقای حالت — یک‌طرفه و با شرط
```
POST /api/v1/appointment-settings/upgrade-booking-mode
{ "schedule_uuid": "…", "confirm": true }
شرایط:
- حالت فعلی slot یا service باشد
- هیچ نوبت pending/confirmed آینده‌ای وجود نداشته باشد
- حداقل یک منبع فعال در شعبه باشد
- سرویس‌های bookable حداقل یک SegmentTemplate یا duration معتبر داشته باشند
بازگشت به حالت قبلی: ممنوع (پاسخ 422)
```
دلیل ممنوعیت بازگشت: نوبت‌های ثبت‌شده در حالت `resource` بخش و اشغال چندمنبعی دارند و
مدل قدیمی نمی‌تواند نمایششان دهد.
## ۷. سقف‌ها و پیش‌فرض‌ها
| پارامتر | پیش‌فرض | سقف |
|---|---|---|
| بازهٔ جستجو | ۳۰ روز | ۹۰ روز (مستند بند ۱۰) |
| `limit` نتایج | ۵۰ | ۲۰۰ |
| گام کاندید | ۱۵ دقیقه | حداقل ۵ |
| منابع کاندید per نیازمندی | — | ۵۰ (بیشتر → `422` با پیشنهاد استفاده از استخر) |
## ۸. edge case ها
| حالت | رفتار درست |
|---|---|
| هیچ نتیجه‌ای در بازه | `data: []` + `reason` (`no_resource`, `fully_booked`, `no_calendar`) — نه ۴۰۴ |
| نقطهٔ شروع دقیقاً روی لبهٔ پنجرهٔ آزاد | معتبر — بازه‌ها نیم‌باز `[s, e)` |
| نوبتی که تازه لغو شده | با کش پنجره‌ای تداخل ندارد چون اشغال کش نمی‌شود |
| `hold` منقضی‌شده در `resource_occupancy` | در کوئری `WHERE status='hold' AND expires_at > :now` رد شود |
| برنامهٔ ۶۰ دقیقه‌ای و پنجرهٔ آزاد ۵۹ دقیقه | هیچ کاندیدی — هرس گام ۳ |
| منبعِ استخری که وسط بازه غیرفعال شده | `findEligible` فقط `active=true` می‌دهد؛ اشغال‌های قبلی‌اش می‌مانند |
| دو نیازمندی هم‌شکل با `count=1` در یک بخش | `groupKey` یکسان → همان منبع دوبار انتخاب می‌شود ← **باگ**. `count=2` بنویس یا `groupKey` را با اندیس نیازمندی درون همان بخش متمایز کن |
| تغییر ساعت رسمی (تغییر ساعت تابستانی) | ایران از ۱۴۰۱ ندارد؛ ولی محاسبات با timestamp انجام شود نه ساعت محلی |
سطر ماقبل آخر یک تلهٔ واقعی است: `groupKey` باید بین **بخش‌ها** یکی باشد ولی درون یک
بخش، دو نیازمندی مجزا دو منبع بگیرند. کلید = `(role, skills, constraints, indexInSegment)`
و تطبیق بین‌بخشی روی سه جزء اول.
## ۹. تست
```
tests/Appointment/Availability/OccupancyIndexTest.php ← واحد، بدون DB
- capacity=3 با ۲ اشغال → جا دارد؛ با ۳ → ندارد
- بازهٔ مماس (end == start) → تداخل نیست
- shared vs exclusive
tests/Appointment/Availability/CandidateGeneratorTest.php
- هرس با تنگ‌ترین منبع
- نقطهٔ گذشته حذف
- برنامه‌ای که در پنجره جا نمی‌شود → هیچ کاندید
tests/Appointment/Availability/ResourceAllocatorTest.php
- منبع مشترک بین بخش ۱ و ۳ → یک نفر
- دو نیازمندی هم‌شکل در یک بخش → دو منبع
- تخصیص ناموفق → null، نه استثنا
tests/Appointment/Availability/CapacityReleaseTest.php ← ⭐ تست پذیرش اصلی
- نوبت الف ۱۰:۰۰-۱۱:۰۰، اپراتور فقط ۱۰:۰۰-۱۰:۰۵ و ۱۰:۳۵-۱۱:۰۰
- جستجوی بیمار ب → زمانی در ۱۰:۰۵-۱۰:۳۵ پیدا شود
tests/Appointment/Availability/StrategyTest.php
- least_gap کمترین شکاف را می‌سازد
- balanced کم‌کارترین را می‌دهد
- preserve_specialists کمترین level کافی را می‌دهد
tests/Appointment/Availability/BookingModeGuardTest.php
- حالت slot روی endpoint جدید → 422 ERR_WRONG_BOOKING_MODE
- endpoint قدیمی در حالت resource → 422
- ارتقا با نوبت فعال آینده → 422
tests/Appointment/AvailabilityPerformanceTest.php
- < 500ms و <= 5 کوئری
tests/Appointment/LegacyBookingUnchangedTest.php
- همهٔ تست‌های موجود appointment-slots و appointment-service-slots سبز بمانند
```
## ۱۰. مستندات
`docs/api/appointment-availability.md` بساز — شامل جدول استراتژی‌ها، توضیح تخصیص حریصانه
و محدودیتش، و ماتریس «کدام endpoint در کدام حالت کار می‌کند».
`docs/api/appointment.md` را با بخش «حالت‌های نوبت‌دهی» به‌روز کن.