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

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