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