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

## تغییر `appointments` — دو ستون تهی‌پذیر

```sql
ALTER TABLE appointments
  ADD COLUMN service_total_minutes SMALLINT NULL,
  ADD COLUMN service_buffer_minutes SMALLINT NULL;
```

| ستون | معنی | چرا لازم است |
|---|---|---|
| `service_total_minutes` | مدت محاسبه‌شدهٔ ترکیب سرویس‌ها در لحظهٔ ثبت | `slot_end - slot_start` عدد را دارد ولی نمی‌گوید عمدی بود یا دستی؛ و برای نوبت رزرو (که `slot_start = slot_end`) هیچ‌جا مدت را نگه نمی‌داریم |
| `service_buffer_minutes` | `buffer_minutes` مؤثر در لحظهٔ ثبت | تغییر بافر در تنظیمات نباید معنای نوبت‌های ثبت‌شده را عوض کند |

هر دو **تهی‌پذیر** و هر دو در حالت اسلاتی `NULL` می‌مانند. هیچ ستون موجودی حذف، تغییر
نوع یا تغییر معنا نمی‌دهد.

⛔ `slot_start` و `slot_end` و `active_slot_key` و `is_reserve` دست‌نخورده. خط سرخ.

### چرا نه یک ستون JSON

وسوسه: یک `service_meta JSON` با همه‌چیز. رد شد چون تسک ۱۴ (گزارش دقت برنامه) روی
`plan_total_minutes` تجمعی می‌زند و JSON را نمی‌تواند `AVG` کند. دو ستون `SMALLINT`
ارزان‌ترند و تسک ۰۷ ستون `plan_total_minutes` را کنارشان اضافه می‌کند
(اسم متفاوت، معنی متفاوت: آن یکی مدت برنامهٔ چندبخشی است).

## ایندکس

هیچ ایندکس جدیدی. `idx_appointments_doctor_slot` و `idx_appointments_tenant_slot` موجود
همهٔ کوئری‌های این تسک را پوشش می‌دهند.

## `appointment_service_items` — بدون تغییر schema

جدول واسط ManyToMany موجود. تنها تغییر، **رفتاری** است:

```php
// Appointment — متد جدید، بدون دست زدن به متدهای موجود
/** جایگزینی کامل سرویس‌ها؛ serviceItem تکی هم با اولی هم‌گام می‌شود. */
public function replaceServiceItems(array $items): self
{
    $this->serviceItems->clear();
    foreach ($items as $item) {
        if (!$this->serviceItems->contains($item)) { $this->serviceItems->add($item); }
    }
    $this->serviceItem = $items[0] ?? null;    // ← سازگاری با مصرف‌کنندهٔ تکی
    $this->updatedAt   = time();
    return $this;
}

/** @return string[] uuid سرویس‌های فعلی، به ترتیب */
public function currentServiceUuids(): array
{
    $uuids = array_map(fn($i) => $i->getUuid(), $this->serviceItems->toArray());
    if ($uuids === [] && $this->serviceItem !== null) { $uuids = [$this->serviceItem->getUuid()]; }
    return $uuids;
}
```

هم‌گام‌سازی `serviceItem` تکی اجباری است: `AppointmentsPage`، `ReserveAppointmentsPage`،
`nobat724_front` و `clinic-pro-tauri` هر چهار روی `service_item` تکی خوانده‌اند. رهاکردنش
یعنی نوبت با سرویس‌های جدید ولی نام سرویس قدیمی در لیست.

## کدهای خطای جدید

در `src/Shared/Constant/ErrorCodes.php`:

```php
public const ERR_SERVICE_DURATION_MISMATCH = 'ERR_APPOINTMENT_010';
// پیام: مدت این نوبت با مجموع مدت سرویس‌های انتخابی نمی‌خواند

public const ERR_WRONG_BOOKING_MODE = 'ERR_APPOINTMENT_011';
// پیام: این عملیات با روش نوبت‌دهی این محیط سازگار نیست
```

شمارهٔ بعدی دامنهٔ `APPOINTMENT` را از خود فایل بگیر، این دو عدد حدسی‌اند.
هر دو کد در تسک‌های ۰۶ و ۰۷ هم استفاده می‌شوند، پس نامشان عمومی است نه مخصوص این تسک.

## Migration

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

## backfill

```bash
ddev exec php bin/console app:appointment:backfill-service-duration          # dry-run
ddev exec php bin/console app:appointment:backfill-service-duration --force
```

برای هر نوبت `pending`/`confirmed` **آیندهٔ** یک محیط سرویسی که `service_total_minutes`
ندارد:

```
service_total_minutes = (slot_end - slot_start) / 60
service_buffer_minutes = buffer_minutes فعلیِ همان برنامه
```

مقدار از خودِ نوبت گرفته می‌شود، **نه از مدت سرویس‌ها** — چون نوبت موجود ممکن است با
مدت دستی ثبت شده باشد و بازمحاسبه یعنی تغییر گذشته.

نوبت‌های اسلاتی و نوبت‌های گذشته رد می‌شوند. idempotent.

## fixture های تست خط سرخ

```
tests/Appointment/fixtures/slot-mode-contract.json           # پاسخ appointment-slots
tests/Appointment/fixtures/month-availability-contract.json  # پاسخ month-availability
tests/Appointment/fixtures/slot-calculator-signatures.php    # امضای متدهای عمومی
```

⛔ این سه فایل بعد از این تسک **read-only** اند. هیچ تسکی اجازهٔ به‌روزرسانی‌شان را ندارد.
اگر تستی قرمز شد، کد باید برگردد نه fixture. این جمله را در بالای هر سه فایل به‌عنوان
کامنت بنویس.

fixture ها با تاریخ ثابت ساخته می‌شوند (`2026-01-05` مثلاً)، نه `time()` — وگرنه فردا قرمز.

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

هیچ entity جدیدی. `appointments` از قبل جفت tenant دارد.
`TenantSchemaCoverageTest` باید بدون تغییر سبز بماند.
