Files
clinicpro/docs/new_feture/taskes/task-06-availability-engine/implementation_notes.md
T
hamed 021d0eb6b2 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.
2026-07-30 11:43:58 +03:30

159 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نکات پیاده‌سازی — تسک ۰۶
## ۱. سه کوئری، بعد هیچ
قاعدهٔ غیرقابل‌مذاکره: **داخل حلقهٔ روز و حلقهٔ نقطهٔ شروع، هیچ 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` را با بخش «حالت‌های نوبت‌دهی» به‌روز کن.