- 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.
159 lines
8.6 KiB
Markdown
159 lines
8.6 KiB
Markdown
# نکات پیادهسازی — تسک ۰۶
|
||
|
||
## ۱. سه کوئری، بعد هیچ
|
||
|
||
قاعدهٔ غیرقابلمذاکره: **داخل حلقهٔ روز و حلقهٔ نقطهٔ شروع، هیچ 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` را با بخش «حالتهای نوبتدهی» بهروز کن.
|