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

## ۱. شعبه ساخته نمی‌شود — پیدا می‌شود

نسخهٔ اول این فایل استدلال می‌کرد «چرا شعبه بالای آدرس می‌نشیند». استدلال درست بود ولی
نتیجه‌اش غلط: اگر آدرس همان مکان فیزیکی است و همه‌جا هم به همان معنا مصرف می‌شود، لایهٔ
بالایی چیزی جز یک جدول دوم برای همان نام و تلفن نیست. سه مصرف‌کنندهٔ زنده‌ای که
`doctor_addresses.id` را می‌خوانند —

1. `WeeklySchedule.setting[day].sessions[].location_id` (JSON)
2. `SlotCalculatorService::buildSessionSlots()` که آن را در هر اسلات کپی می‌کند
3. `AppointmentController::bookingLocations()` که به سایت عمومی `location_uuid` می‌دهد

— دلیل اصلی‌اند که «شعبه» همان آدرس است، نه دلیلِ ساختن لایهٔ دوم.
جزئیات کامل: [`_shared/branch-is-doctor-address.md`](../_shared/branch-is-doctor-address.md).

## ۲. پزشک مستقل هم شعبه دارد

خوشبختانه از قبل درست است: `DoctorAddress::forDoctor()` با `type = 'personal'` وجود دارد و
`findForContext()` هم آدرس‌های شخصی و هم کلینیکی را برمی‌گرداند. پس تسک ۰۲ لازم نیست دو
مسیر کد برای «منبع مال شعبه» و «منبع مال پزشک» داشته باشد. مطب شخصی = آدرسی با
`type='personal'`.

## ۳. `TenantFilter` روی `doctor_addresses` کار نمی‌کند

مهم‌ترین تلهٔ این تسک. `doctor_addresses` ستون `entity_type`/`entity_id` ندارد، پس:

```php
// ❌ آدرس محیط دیگر را هم برمی‌گرداند — filter اینجا تور ایمنی نیست
$address = $this->addresses->findOneBy(['uuid' => $uuid]);

// ✅ از BranchResolver رد شو
$address = $this->branches->resolve($uuid);   // 404 اگر محیط جاری نباشد
```

`Room` خودش `TenantOwnedTrait` دارد (uuidش از request می‌آید) پس روی آن filter کار می‌کند؛
ولی `BranchWorkingHours` فرزند aggregate است و **هیچ** فیلتری ندارد — تنها محافظش این است
که فقط از راه `BranchResolver` قابل دسترسی باشد. `RequestReachableChildTenantTest` همین را
اجبار می‌کند: هیچ endpointی نباید uuid فرزند را مستقیم بگیرد.

## ۴. حذف

روی حذف آدرس هیچ دست نمی‌بریم (endpointهایش موجودند). فقط:

- `branch_working_hours.address_id` و `rooms.address_id` هر دو `ON DELETE CASCADE` — حذف
  آدرس ساعت و اتاقش را هم می‌برد. این درست است: ساعت کاری بدون مکان معنا ندارد.
- حذف **اتاق**: در این تسک بی‌قید (منبعی هنوز وجود ندارد). سرویس را طوری بنویس که افزودن
  گاردهای تسک ۰۲ (منبع فعال) و ۰۷ (نوبت آینده) یک خط باشد — آرایهٔ تزریقی از
  `RoomDeletionGuardInterface`، نه زنجیرهٔ `if`.
- `active=false` مسیر اصلی است، نه `DELETE`.

⚠️ CASCADE روی حذف آدرس + وجود نوبت روی اتاق‌های آن = دادهٔ گم‌شده. تا تسک ۰۷ که نوبت به
اتاق وصل می‌شود، این ریسک وجود ندارد؛ آنجا باید گاردِ حذف آدرس اضافه شود. در checklist با
مقصد صریح ثبت شده.

## ۵. ساعت کاری — دقیقه، نه رشته

```php
// ❌ اشتباه: مقایسهٔ رشته‌ای در تسک ۰۶ می‌شکند ("9:00" < "10:00" غلط است)
private string $startTime = '09:00';

// ✅ درست
private int $startMinute = 540;
```

اعتبارسنجی در `WorkingHoursService`:
- `0 <= start < end <= 1440`
- بازه‌های یک روز نباید هم‌پوشانی داشته باشند (مرتب کن، بعد `prev.end <= next.start`)
- `sequence` را خود سرویس بعد از مرتب‌سازی تخصیص می‌دهد، نه کلاینت
- **اعتبارسنجی کاملِ هر هفت روز قبل از هر `DELETE`** — وگرنه یک بازهٔ نامعتبر در روز ششم،
  شش روز درست را هم پاک می‌کند و ۴۲۲ برمی‌گرداند

## ۶. تفسیر «شعبه بدون ساعت کاری»

تصمیم صریح: **تعریف‌نشده، نه همیشه‌باز.** تسک ۰۳ وقتی برای آدرسی ساعتی پیدا نکرد، به
رفتار فعلی برمی‌گردد (برنامهٔ پزشک تنها مرجع است). پس همهٔ دادهٔ موجود — که هیچ ساعت کاری
شعبه ندارد — دقیقاً مثل امروز کار می‌کند. این خطِ دفاعیِ «منطق اسلاتی دست نمی‌خورد» است.

همین‌طور `active=false` روی آدرس در این تسک **هیچ اثری بر اسلات ندارد**؛ فقط ذخیره می‌شود.
اعمالش در تسک ۰۳ است. اگر همین‌جا اعمال شود، `SlotCalculatorService` عوض می‌شود که در
`_shared/red-lines.md` ممنوع است.

هر دو نکته در `docs/api/branch.md` نوشته شود، وگرنه اولین کسی که دیباگ می‌کند فکر می‌کند باگ است.

## ۷. edge case ها

| حالت | رفتار درست |
|---|---|
| آدرس محیط A، اتاق ساخته‌شده با uuid آدرس محیط B | `404` — `BranchResolver` قبل از هر کاری |
| دو آدرس هم‌نام در یک محیط | مجاز (نام یکتا نیست) |
| `capacity = 0` یا منفی | `422` — حداقل ۱ |
| ساعت کاری روز جمعه خالی | معتبر — یعنی شعبه جمعه بسته است |
| `PUT` با آرایهٔ خالی | همهٔ ساعت‌ها پاک می‌شوند — شعبه کامل بسته |
| ساعت شبانه‌روزی | `start=0, end=1440` — یک ردیف، نه دو |
| آدرسی که تنها آدرس فعال محیط است و غیرفعال می‌شود | مجاز، ولی هشدار در UI |
| `timezone` نامعتبر مثل `"Tehran"` | `422` — با `DateTimeZone::listIdentifiers()` چک کن، نه regex |

## ۸. تست

```
tests/Branch/WorkingHoursTest.php
  - هفت روز معتبر → 200 و بازخوانی یکسان (کلیدهای 0..6)
  - end <= start → 422
  - دو بازهٔ هم‌پوشان در یک روز → 422
  - بازهٔ شبانه‌روزی 0..1440 → 200
  - آرایهٔ خالی → 200 و صفر ردیف
  - بازهٔ نامعتبر در روز ششم → 422 و شش روز قبلی دست‌نخورده (اتمی بودن)
  - uuid آدرس محیط دیگر → 404
tests/Branch/RoomCrudTest.php
  - ساخت با نقش مالک کلینیک → 201 و جفت tenant مشتق از آدرس
  - capacity=0 → 422
  - آدرس محیط دیگر → 404
  - ویرایش/حذف اتاق محیط دیگر → 404
tests/Branch/BranchAddressFieldsTest.php
  - آدرس موجود بدون مقدار → active=true و timezone='Asia/Tehran'
  - timezone نامعتبر → 422
tests/Appointment/SlotModeFrozenTest.php   ← باید سبز بماند (اسلات دست‌نخورده)
tests/Shared/TenantSchemaCoverageTest.php  ← باید سبز بماند
tests/Shared/TenantLookupInventoryTest.php ← repository جدید باید ثبت شود
```

اجرا:
```bash
ddev exec php bin/phpunit tests/Branch
ddev exec php bin/phpunit --group=slot-mode-frozen
ddev exec php vendor/bin/phpstan analyse src/Branch
```

⚠️ قبل از اجرای تست، ستون‌ها و جدول‌ها را دستی روی `db_test` بساز — رجوع به بخش
Migration در [database.md](database.md). `db_test` تاریخچهٔ migration جدا دارد.

## ۹. مستندات

`docs/api/branch.md` بساز (الگو: `docs/api/staff.md`). در `docs/api/README.md` اضافه کن.
`docs/api/doctor.md` را برای دو فیلد جدید `DoctorAddress::toArray()` به‌روز کن — این فیلدها
در پاسخ ۹ endpoint موجود آدرس ظاهر می‌شوند، پس تغییر قرارداد است.
در `docs/architecture/tenancy.md` جدول طبقه‌بندی را با دو جدول جدید به‌روز کن.
