# دیتابیس — تسک ۰۰ ## تغییر `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` باید بدون تغییر سبز بماند.