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

134 lines
7.1 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.
# نکات پیاده‌سازی — تسک ۰۳
## ۱. `IntervalSet` اول، بقیه بعد
اولین چیزی که می‌نویسی `src/Shared/Time/IntervalSet.php` و تستش است — قبل از هر entity.
سه تسک بعدی روی درستی‌اش حساب می‌کنند و باگ مرزی در تفاضل بازه، در تسک ۰۶ به شکل
«یک اسلات عجیب» ظاهر می‌شود که دیباگش ساعت‌ها می‌برد.
مرزهایی که تست واحد باید بپوشاند:
```php
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` یعنی بازهٔ تهی و حذف می‌شود.
## ۲. شعبهٔ بدون ساعت کاری = بی‌قید، نه بسته
```php
$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` دارد.
```php
$tz = new \DateTimeZone($branch->getTimezone());
$day = (new \DateTimeImmutable("@{$ts}"))->setTimezone($tz);
$dow = ((int) $day->format('w') + 1) % 7; // ← همان تبدیل SlotCalculatorService
```
تبدیل `(w + 1) % 7` عمداً همان است که در `SlotCalculatorService:359` هست. دو قرارداد
شمارش روز هفته در یک کدبیس = باگ قطعی.
## ۵. کارایی — پنج کوئری، نه پنج × تعداد روز
```php
// ❌ اشتباه
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` یک بخش
«تفاوت با تقویم منبع» اضافه کن تا کسی دو سیستم را قاطی نکند.