# نکات پیادهسازی — تسک ۰۰ ## ۱. اول تست خط سرخ، بعد هر چیز دیگر ترتیب کار: ``` ۱. tests/Appointment/SlotModeFrozenTest.php + سه fixture ← اول این ۲. ddev exec php bin/phpunit --group=slot-mode-frozen ← باید سبز باشد قبل از هر تغییری ۳. بقیهٔ تسک ۴. دوباره گام ۲ — باید همچنان سبز باشد ``` اگر fixture را بعد از تغییرات بسازی، هیچ چیزی را تضمین نکردهای — snapshot وضعیت تغییریافته را گرفتهای. ## ۲. `isServiceMode` گِیت همهچیز است هر خط کد جدید در مسیر مشترک باید داخل این شرط باشد: ```php if ($this->serviceCalc->isServiceMode($doctor, $clinic)) { // … منطق جدید } ``` نه بیرونش، نه با `??`، نه با «اگر سرویس دارد». معیار **فقط** `booking_mode` است. نوبت اسلاتی هم میتواند `service_item_id` داشته باشد (فیلدهای Figma نوبتها) — آن دلیل سرویسی بودن نیست. اشتباه رایج: ```php // ❌ نوبت اسلاتیِ دارای سرویس را وارد مسیر جدید میکند if ($appointment->getServiceItems()->count() > 0) { … } ``` ## ۳. جمع سادهٔ مدت را همینجا اصلاح نکن ```php $totalMinutes += $duration; // ← اشتباه است، ولی دست نزن ``` مستند بند ۵ میگوید این فرمول ظرفیت را الکی پر میکند و راهحلش «زمان تنها / زمان اضافه» است — که تسک ۰۴ میسازد. اصلاحش اینجا یعنی: - مدت همهٔ نوبتهای چندسرویسیِ در حال رزرو یکشبه کم میشود - سایت و اپ دسکتاپ عدد متفاوت میبینند بدون اینکه چیزی در build بشکند - و هیچ دادهای برای «زمان اضافه» وجود ندارد، پس اصلاح بیورودی غیرممکن است کاری که این تسک میکند: محاسبه را به **یک نقطه** منتقل میکند تا تسک ۰۴ یک خط عوض کند. ## ۴. `excludeAppointmentId` — الگوی موجود را تکرار کن `AppointmentRepository::isSlotTaken($doctor, $start, $end, $excludeId)` از قبل این پارامتر را دارد. `findBusyIntervals` هم همان را بگیرد، با همان نام و همان جای پارامتر و همان پیشفرض `null`. دو الگوی متفاوت برای یک کار (مثلاً یکی `?int $excludeId`، دیگری `array $excludeIds`) یعنی اولین کسی که هر دو را میبیند یکی را اشتباه صدا میزند. ## ۵. اعتبارسنجی زمان: عضویت در فهرست، نه «اشغال نبودن» ```php // ❌ ناکافی if ($this->appointmentRepo->isSlotTaken($doctor, $start, $end, $excludeId)) { /* 409 */ } // ✅ $starts = $this->slotCalculator->getServiceStartTimes(…); if (!in_array($req->start, array_column($starts, 'start'), true)) { /* 422 */ } ``` `isSlotTaken` فقط تداخل با نوبت دیگر را میگوید. `getServiceStartTimes` علاوه بر آن شیفت، تعطیلی، `date_override`، پنجرهٔ رزرو و بافر را هم اعمال میکند. با شرط اول، منشی میتواند نوبت را ساعت ۳ بامداد بگذارد. ## ۶. `warnings[]` بهجای `422` برای سرویس غیرفعال در نوبت موجود ```php $duration = $this->serviceCalc->calculate(…, allowInactive: true); // $duration->warnings === ['سرویس «لیزر صورت» دیگر برای نوبتدهی فعال نیست'] ``` نوبت موجود با سرویسی که کلینیک غیرفعالش کرده، باید قابل جابهجایی و لغو بماند. `422` یعنی آن نوبت برای همیشه قفل میشود و منشی هیچ کاری نمیتواند بکند. ولی **افزودن** سرویس غیرفعال به نوبت → `422`. تفاوتش `allowInactive` است که فقط برای uuid های موجودِ نوبت `true` میشود، نه برای uuid های تازهی درخواست. ## ۷. `replaceServiceItems` باید `serviceItem` تکی را همگام کند ```php $this->serviceItem = $items[0] ?? null; ``` چهار مصرفکننده روی `service_item` تکی خواندهاند (`AppointmentsPage`، `ReserveAppointmentsPage`، `nobat724_front/services/response.js`، `clinic-pro-tauri/src/service/response.js`). این دقیقاً همان الگویی است که `ServiceItem::setStaffMembers()` برای `staff` تکی دارد — تکرارش کن. ## ۸. `refreshActiveSlotKey` پس از `setIsReserve(false)` ```php public function setIsReserve(bool $v): self { $this->isReserve = $v; $this->refreshActiveSlotKey(); // ← اگر نیست، اضافه کن $this->updatedAt = time(); return $this; } ``` بدون آن، رزروِ تبدیلشده `active_slot_key = NULL` میماند و دو نفر میتوانند همان ساعت را بگیرند. **این تنها تغییر مجاز در مکانیزم `active_slot_key` است** و فقط چون یک شرط موجود را اعمال میکند، نه عوضش میکند. تست: `ConvertReserveSlotKeyTest`. ## ۹. تبدیل رزرو، اتمی ```php $this->em->wrapInTransaction(function () use ($reserve, $req) { $duration = …; // یا اسلات اسلاتی $reserve->setIsReserve(false); $reserve->reschedule($req->start, $end); $reserve->replaceServiceItems($duration->serviceItems); // UniqueConstraintViolationException روی active_slot_key → 409 }); ``` `try/catch` روی `UniqueConstraintViolationException` و ترجمه به `409 ERR_SLOT_TAKEN` — همان چیزی که `SlotTakenException` موجود در `src/Appointment/Repository/` انجام میدهد. از همان استفاده کن. ## ۱۰. `ReserveAppointmentsPage` → `DataTable` صفحه امروز جدول خام با `