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

8.6 KiB
Raw Blame History

نکات پیاده‌سازی — تسک ۰۶

۱. سه کوئری، بعد هیچ

قاعدهٔ غیرقابل‌مذاکره: داخل حلقهٔ روز و حلقهٔ نقطهٔ شروع، هیچ 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 منبع است، ولی منبع در لحظهٔ بررسی هنوز انتخاب نشده. پس دو گذر:

  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:

$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 را با بخش «حالت‌های نوبت‌دهی» به‌روز کن.