- 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.
134 lines
7.1 KiB
Markdown
134 lines
7.1 KiB
Markdown
# نکات پیادهسازی — تسک ۰۳
|
||
|
||
## ۱. `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` یک بخش
|
||
«تفاوت با تقویم منبع» اضافه کن تا کسی دو سیستم را قاطی نکند.
|