# دیتابیس — تسک ۰۱

MariaDB 11.8 · Doctrine ORM 3.6 · همهٔ timestamp ها `INT` (Unix)

> ⛔ **جدول `branches` ساخته نمی‌شود.** «شعبه» همان `doctor_addresses` است — دلیل کامل در
> [`_shared/branch-is-doctor-address.md`](../_shared/branch-is-doctor-address.md). آنچه در
> نسخهٔ اول این فایل به‌عنوان جدول `branches` و ستون `doctor_addresses.branch_id` آمده بود
> حذف شد.

## تغییر جدول موجود — `doctor_addresses`

```sql
ALTER TABLE doctor_addresses
  ADD COLUMN active   TINYINT(1)  NOT NULL DEFAULT 1,
  ADD COLUMN timezone VARCHAR(40) NOT NULL DEFAULT 'Asia/Tehran';
```

هیچ ستونی حذف یا تغییر نوع نمی‌دهد. هر دو `NOT NULL DEFAULT` دارند، پس ردیف‌های موجود
بی‌نیاز از backfill درست می‌شوند و هیچ رفتار فعلی عوض نمی‌شود.

`timezone` از همین حالا اضافه می‌شود چون بند ۹ مستند ذخیره‌سازی UTC با نمایش محلی می‌خواهد؛
افزودنش بعد از اینکه داده‌های زمان‌دار روی شعبه نشستند یعنی backfill پرریسک.

⚠️ `active` در این تسک **فقط ذخیره** می‌شود؛ فیلتر شدنش در محاسبهٔ اسلات کارِ تسک ۰۳ است
(هر تغییری در `SlotCalculatorService` در فاز فعلی ممنوع است — `_shared/red-lines.md`).

## `branch_working_hours`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `address_id` | INT NOT NULL | FK → `doctor_addresses.id` ON DELETE CASCADE |
| `day_of_week` | TINYINT NOT NULL | ۰=شنبه … ۶=جمعه — همان قرارداد `SlotCalculatorService` |
| `sequence` | TINYINT NOT NULL DEFAULT 0 | بازهٔ چندم آن روز |
| `start_minute` | SMALLINT NOT NULL | ۰..۱۴۴۰ از نیمه‌شب |
| `end_minute` | SMALLINT NOT NULL | > `start_minute` |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |

```sql
UNIQUE KEY uniq_bwh_address_day_seq (address_id, day_of_week, sequence)
KEY        idx_bwh_address_day      (address_id, day_of_week, active)
```

بدون ستون tenant — **فرزند aggregate** با ریشهٔ `DoctorAddress`. هرگز با uuid از request
لود نمی‌شود؛ تنها راه رسیدن به آن `/branch/{addressUuid}/working-hours` است که آدرس را از
`TenantFilter` رد می‌کند. در `GlobalTables::AGGREGATE_CHILDREN` با ریشهٔ صریح ثبت می‌شود.

دقت: چون `doctor_addresses` خودش `TenantOwnedTrait` ندارد (مالکیتش با `type` +
`doctor_id`/`clinic_id` است)، `TenantFilter` روی خودِ آدرس هم اعمال نمی‌شود.
پس فیلتر محیط برای این endpoint **دستی** است: `DoctorAddressRepository::findForContext()`
که از قبل همین کار را می‌کند و در `clinic/{uuid}/addresses` هم همین‌طور استفاده شده.

## `rooms`

| ستون | نوع | توضیح |
|---|---|---|
| `id` | INT PK AI | |
| `uuid` | VARCHAR(36) UNIQUE | **از request می‌آید** → پس جفت tenant لازم دارد |
| `entity_type` / `entity_id` | VARCHAR(10)/INT NOT NULL | در سازنده از `DoctorAddress` مشتق می‌شود |
| `address_id` | INT NOT NULL | FK → `doctor_addresses.id` ON DELETE CASCADE |
| `name` | VARCHAR(120) NOT NULL | |
| `room_type` | VARCHAR(60) NULL | متن آزاد، تعریف کلینیک |
| `capacity` | SMALLINT NOT NULL DEFAULT 1 | ظرفیت هم‌زمان |
| `floor` | VARCHAR(20) NULL | ویژگی آزاد — مستند بند ۶ |
| `active` | TINYINT(1) NOT NULL DEFAULT 1 | |
| `created_at` / `updated_at` | INT NOT NULL | |

```sql
KEY idx_rooms_tenant  (entity_type, entity_id, active)
KEY idx_rooms_address (address_id, active)
```

`entity_type, entity_id` ستون‌های اولِ ایندکس‌اند — شرط `TenantFilter` وگرنه از ایندکس
استفاده نمی‌کند.

اشتقاق جفت در سازنده (نه از بدنهٔ request):

```php
$isClinic = $address->getType() === DoctorAddress::TYPE_CLINIC;
$this->assignTenantPair(
    $isClinic ? 'clinic' : 'doctor',
    $isClinic ? (int) $address->getClinicId() : (int) $address->getDoctor()->getId(),
);
```

## Migration

```bash
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
```

هیچ command backfill لازم نیست — `app:branch:backfill` نسخهٔ اول برای پر کردن `branch_id`
از `doctor_addresses` بود؛ حالا که جدول موازی ساخته نمی‌شود، موضوعش منتفی است.

⚠️ **`db_test` تاریخچهٔ migration جدا دارد** و `migrate` رویش با
`Table 'users' already exists` می‌شکند. ستون‌ها را دستی اضافه کن وگرنه کل تست‌سوئیت با
`Unknown column` قرمز می‌شود:

```bash
ddev mysql -uroot -proot -e "ALTER TABLE db_test.doctor_addresses \
  ADD active TINYINT(1) NOT NULL DEFAULT 1, \
  ADD timezone VARCHAR(40) NOT NULL DEFAULT 'Asia/Tehran';"
```
و بعد از تولید migration، همان `CREATE TABLE` های `branch_working_hours` و `rooms` را هم
روی `db_test` اجرا کن.

## طبقه‌بندی tenant

| جدول | وضعیت | ثبت در |
|---|---|---|
| `doctor_addresses` | از قبل طبقه‌بندی‌شده — دست نمی‌خورد | همان‌جای فعلی |
| `rooms` | جفت tenant | `TenantOwnedTrait` (uuid از request می‌آید) |
| `branch_working_hours` | فرزند aggregate | `GlobalTables::AGGREGATE_CHILDREN` → ریشه `DoctorAddress` |

بعد از migration:
```bash
ddev exec php bin/phpunit tests/Shared/TenantSchemaCoverageTest.php
ddev exec php bin/phpunit tests/Shared/TenantLookupInventoryTest.php
```
دومی هم لازم است: repository جدیدی که `findByUuid` دارد باید در `REVIEWED` ثبت شود.
