- 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.
8.6 KiB
نکات پیادهسازی — تسک ۰۶
۱. سه کوئری، بعد هیچ
قاعدهٔ غیرقابلمذاکره: داخل حلقهٔ روز و حلقهٔ نقطهٔ شروع، هیچ I/O نیست.
// ❌ مرگ کارایی
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 — بازهٔ اشغال، نه بازهٔ بخش
$occStart = $segmentStart - $resource->getSetupMinutes() * 60;
$occEnd = $segmentEnd + $resource->getCleanupMinutes() * 60;
$index->hasRoom($resourceId, $occStart, $occEnd, $units);
نکتهٔ ظریف: setup/cleanup per منبع است، ولی منبع در لحظهٔ بررسی هنوز انتخاب نشده.
پس دو گذر:
- بررسی اولیه با بیشینهٔ
setup/cleanupکاندیدها (محافظهکار) - بعد از انتخاب منبع، بازهٔ دقیق همان منبع محاسبه و دوباره بررسی شود
گذر دوم ارزان است (یک منبع، یک بازه) و از رد شدن اشتباه کاندیدها جلوگیری میکند.
۳. capacity و units
exclusive → units = capacity (منبع کامل)
shared → units = 1
passive → units = capacity (رزرو است، ولی پرچم passive برای گزارش)
hasRoom جمع units اشغالهای متداخل را با capacity مقایسه میکند. با این مدل،
اتاق تزریق سهتخته با یک ردیف کار میکند و شمارش خودکار است.
۴. منبع مشترک بین بخشها
مثال مستند: اپراتور در بخش ۱ (۰-۵) و بخش ۳ (۳۵-۵۵) لازم است، در بخش ۲ نه.
- باید همان اپراتور باشد →
groupKeyدرResourceAllocator - در بخش ۲ نباید اشغال بماند → دو ردیف اشغال جدا، نه یکی از ۰ تا ۵۵
اگر ردیف را یکی کنی، کل ارزش این پروژه از بین میرود: همان ۳۰ دقیقهای که میخواستیم آزاد کنیم دوباره قفل میشود. تست پذیرش «آزادسازی ظرفیت» دقیقاً همین را میسنجد.
۵. مسیر قدیمی دستنخورده
SlotCalculatorService هیچ تغییری نمیکند. AvailabilityEngine یک کلاس جدید کنارش است.
انتخاب بین این دو فقط در کنترلر و بر اساس booking_mode:
$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 را با بخش «حالتهای نوبتدهی» بهروز کن.