Files
clinicpro/docs/new_feture/taskes/task-03-resource-calendar/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

7.1 KiB
Raw Blame History

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

۱. IntervalSet اول، بقیه بعد

اولین چیزی که می‌نویسی src/Shared/Time/IntervalSet.php و تستش است — قبل از هر entity. سه تسک بعدی روی درستی‌اش حساب می‌کنند و باگ مرزی در تفاضل بازه، در تسک ۰۶ به شکل «یک اسلات عجیب» ظاهر می‌شود که دیباگش ساعت‌ها می‌برد.

مرزهایی که تست واحد باید بپوشاند:

subtract([[0,100]], [[0,100]])      === []              // کامل
subtract([[0,100]], [[20,30]])      === [[0,20],[30,100]] // وسط
subtract([[0,100]], [[100,200]])    === [[0,100]]       // مجاور، نه متداخل
subtract([[0,100]], [[-10,10]])     === [[10,100]]      // از چپ بیرون‌زده
intersect([[0,100]], [])            === []              // خالی = هیچ، نه همه‌چیز
normalize([[0,50],[50,100]])        === [[0,100]]       // ادغام مجاور
normalize([[10,20],[0,5]])          === [[0,5],[10,20]] // مرتب‌سازی

قرارداد بازه‌ها: نیم‌باز [start, end). همه‌جا. end == start یعنی بازهٔ تهی و حذف می‌شود.

۲. شعبهٔ بدون ساعت کاری = بی‌قید، نه بسته

$branchHours = $this->branchHoursRepo->forDay($branch, $dow);
$windows = $branchHours === []
    ? $resourceShifts                                   // بی‌قید
    : IntervalSet::intersect($resourceShifts, $branchHours);

اگر برعکسش را بنویسی، همهٔ داده‌های موجود (که هیچ شعبه‌ای ساعت کاری ندارد) یک‌شبه هیچ وقتی نمی‌دهند. این تصمیم در تسک ۰۱ هم نوشته شده — هر دو جا باید یکی باشد.

۳. تعطیلات: ملی، محیطی، منبعی — ترتیب

روز تعطیل است اگر:
    (در national_holidays با is_official=true باشد
     و tenant_holiday_override با is_working=true نداشته باشد)
  یا
    (tenant_holiday_override با is_working=false داشته باشد)

منبع می‌تواند با resource_exception روزِ باز را ببندد، ولی نمی‌تواند روز تعطیل را باز کند. باز کردن فقط در سطح محیط معنی دارد (کل کلینیک آن روز کار می‌کند یا نه).

۴. زمان و منطقهٔ زمانی

ذخیره‌سازی همیشه Unix timestamp (UTC ذاتی). date('w', $ts) و strtotime('today') به منطقهٔ زمانی PHP وابسته‌اند. SlotCalculatorService امروز روی همین فرض کار می‌کند و php.ini پروژه روی Asia/Tehran است.

قاعده: هیچ‌جا date() بدون منطقهٔ زمانی صریح ننویس وقتی شعبه timezone دارد.

$tz  = new \DateTimeZone($branch->getTimezone());
$day = (new \DateTimeImmutable("@{$ts}"))->setTimezone($tz);
$dow = ((int) $day->format('w') + 1) % 7;   // ← همان تبدیل SlotCalculatorService

تبدیل (w + 1) % 7 عمداً همان است که در SlotCalculatorService:359 هست. دو قرارداد شمارش روز هفته در یک کدبیس = باگ قطعی.

۵. کارایی — پنج کوئری، نه پنج × تعداد روز

// ❌ اشتباه
foreach ($days as $day) { $exceptions = $repo->findForDay($resource, $day); }

// ✅ درست — یک بار برای کل بازه، بعد در حافظه
$exceptions = $repo->findOverlapping($resource, $from, $to);
$byDay = $this->bucketByDay($exceptions, $from, $to);

معیار: rawWindows() برای یک منبع در ۹۰ روز باید دقیقاً ۵ کوئری بزند. یک تست با ProfilerStack یا شمارندهٔ SQLLogger این را قفل کند — وگرنه اولین refactor آن را می‌شکند.

۶. سازگاری با نوبت‌دهی موجود

این تسک هیچ‌چیز از SlotCalculatorService را تغییر نمی‌دهد. ResourceAvailabilityService یک سرویس موازی است که فقط در حالت booking_mode=resource (تسک ۰۶) صدا زده می‌شود.

تنها نقطهٔ اتصال: HolidayResolver می‌تواند از تسک بعد در SlotCalculatorService هم استفاده شود تا پزشک‌های حالت slot هم تعطیلات رسمی را بگیرند. آن یک تغییر رفتاری است (روزهایی که امروز باز بودند بسته می‌شوند) — پس در این تسک انجام نده، به‌عنوان یک تغییر جدا با تأیید محصول.

۷. edge case ها

حالت رفتار درست
منبع بدون هیچ resource_calendar rawWindows خالی، explainEmptyDay = no_calendar
استثنا که کل روز را می‌پوشاند آن روز خالی
استثنا نیم‌روزه فقط همان بازه کسر شود
دو استثنای هم‌پوشان union بعد subtract — نه کسر پشت‌سرهم (دوبار کسر بازهٔ مشترک)
شیفت با valid_to گذشته نادیده گرفته شود
شیفت شبانه (۲۲:۰۰ تا ۰۲:۰۰) پشتیبانی نمی‌شود در این تسک422 با پیام روشن. دو ردیف بنویسند
from > to 422
بازهٔ بزرگ‌تر از ۹۰ روز 422 — سقف مستند بند ۱۰
تعطیل رسمی + override محیط + استثنای منبع، هر سه روی یک روز ترتیب بند ۳ بالا

شیفت شبانه عمداً بیرون است: پشتیبانی‌اش یعنی هر بازهٔ روزانه ممکن است به روز بعد سرریز کند و کل منطق bucket-by-day باید بازنویسی شود. اگر کلینیکی لازم داشت، تسک جدا.

۸. تست

tests/Shared/Time/IntervalSetTest.php          ← اول این، واحد و بدون DB
tests/Resource/ResourceCalendarTest.php
  - PUT شیفت‌ها، بازخوانی یکسان
  - end <= start → 422 · هم‌پوشانی در یک روز → 422
  - شیفت شبانه → 422
tests/Resource/ResourceAvailabilityTest.php
  - منبع بدون تقویم → خالی + دلیل no_calendar
  - تقاطع با ساعت شعبه
  - شعبهٔ بدون ساعت → بی‌قید
  - کسر استثنای نیم‌روزه
  - دو استثنای هم‌پوشان → یک بار کسر
tests/Resource/AvailabilityQueryCountTest.php
  - ۹۰ روز → دقیقاً ۵ کوئری
tests/Holiday/HolidayResolverTest.php
  - تعطیل رسمی برای همه
  - override is_working=true فقط برای همان محیط
  - override is_working=false روی روز غیررسمی
tests/Holiday/ImportNationalHolidaysTest.php
  - idempotent

۹. مستندات

docs/api/resource-calendar.md بساز. در docs/api/appointment-settings.md یک بخش «تفاوت با تقویم منبع» اضافه کن تا کسی دو سیستم را قاطی نکند.