Task 01 planned a new `branches` table with `doctor_addresses.branch_id` bridging to it. That plan was wrong: the branch already exists and is called `DoctorAddress`. It carries name, address, telephone, coordinates, city/province FKs and an owner (`forDoctor` / `forClinic` + `type`), and the whole system already consumes it with exactly that meaning — `WeeklySchedule.sessions[].location_id` points at `doctor_addresses.id`, `appointment-booking-locations` calls each row a booking location, and nine CRUD endpoints plus four admin pages manage them. A parallel table would mean two sources of truth for one physical place and a branch that `location_id` never references. So no `branches` table and no duplicate branch CRUD. Only the three genuinely missing pieces: - `doctor_addresses.active` / `.timezone`, both NOT NULL with a default so existing rows need no backfill and no current behaviour changes. `active` is stored only — applying it to slot calculation is task 03, since touching `SlotCalculatorService` is off limits in this phase. - `branch_working_hours`, keyed to `doctor_addresses.id`. Minutes from midnight rather than "09:00" strings so range intersection stays arithmetic. PUT replaces all seven days; validation of the whole week runs before any DELETE, so an invalid sixth day cannot wipe the five valid ones and then answer 422. - `rooms`, with `capacity` as concurrency (a three-bed injection room is one resource with capacity 3, not three resources) and a deletion-guard iterator so tasks 02 and 07 can add reasons without editing RoomService. `BranchWorkingHours` first registered as an aggregate child of `DoctorAddress`; TenantSchemaCoverageTest rejected it correctly, because that root is itself declared global. It now carries a real tenant pair instead, derived in the constructor from the address's `type` — a total mapping, and the address is only ever listed in its own context, so nothing is hidden wrongly. RoomController checks ownership explicitly rather than trusting TenantFilter: hard isolation only applies to a *chosen* context, so a doctor who had not selected one could PATCH another clinic's room. Caught by RoomCrudTest::testForeignRoomIsNotFound, which failed with 200 before the fix. 35 tests, 97 assertions. Slot-mode frozen contract still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.1 KiB
تسک ۰۱ — شعبه و اتاق
فاز: ۱ (هسته) · وابستگی: ۰۰ · زمان: ۶-۸ ساعت (بازتخمین — رجوع به بخش «تصحیح طرح»)
⛔ تصحیح طرح (اجرای ۱۴۰۵/۰۵/۰۸)
نسخهٔ اول این تسک یک جدول branches تازه میخواست و doctor_addresses.branch_id را به
آن وصل میکرد. این طرح غلط بود: «شعبه» از قبل وجود دارد و نامش DoctorAddress است.
branches پیشنهادی |
واقعیت doctor_addresses |
|---|---|
name · phone · address |
✅ name · telephone · address |
city_id · province_id |
✅ FK به entity City/Province — بهتر از ارجاع خام به categories که طرح اول میخواست |
latitude · longitude |
✅ هر دو |
| جفت tenant | ⚠️ DoctorAddress::forDoctor(Doctor) / forClinic(int $clinicId) + ستون type — همان اطلاعات، با شکل دیگر |
timezone |
❌ غایب |
active |
❌ غایب |
| ساعت کاری | ❌ غایب — واقعاً جدید |
| اتاق | ❌ غایب — واقعاً جدید |
و اینها هم از قبل هستند:
WeeklySchedule.setting[day].sessions[].location_id→doctor_addresses.idGET /api/v1/appointment-booking-locations/{doctorUuid}هر آدرس را «محل نوبتدهی» مینامدDoctorAddressRepository::findForContext($doctor, $clinicId)چند آدرس per محیط میدهد- ۹ endpoint CRUD:
clinic/{uuid}/addresses(۴ عدد) وclinic-pro/doctor-address*(۵ عدد) - UI ادمین در
ClinicDetailPage،ClinicFormPage،DoctorDetailPage،SettingsPage
ساختن branches بالای این یعنی: دو جدول برای یک مکان فیزیکی، دو منبع حقیقت برای
نام/آدرس/تلفن/مختصات، هر مصرفکننده باید تصمیم بگیرد کدام را بخواند، و branchی که
location_id هرگز به آن اشاره نمیکند. نقض قاعدهٔ #۸ پروژه
(«اول بگرد، بعد توسعه بده، در آخر بساز»).
پس در این تسک هیچ جدول branches ساخته نمیشود و هیچ endpoint CRUD شعبه اضافه نمیشود.
هدف
سه چیزِ واقعاً غایب را اضافه کن تا تسکهای ۰۲ و ۰۳ جایی برای نشستن داشته باشند:
۱. active و timezone روی DoctorAddress — یک شعبهٔ بسته باید بتواند بسته شود، و
بند ۹ مستند ذخیرهسازی UTC با نمایش محلی میخواهد.
۲. ساعت کاری هفتگی شعبه — امروز ساعت کاری فقط روی برنامهٔ پزشک است. تسک ۰۳ برای
کسر لایهها به ساعت کاری شعبه نیاز دارد.
۳. اتاق — با ظرفیت همزمان. تسک ۰۲ اتاق را بهعنوان یک resource_type منعکس میکند.
دامنه
هست: دو ستون روی DoctorAddress · جدول و entity BranchWorkingHours · جدول و entity
Room · سرویس اعتبارسنجی ساعت · endpoint ساعت کاری و CRUD اتاق · UI ادمین برای هر دو.
نیست: جدول branches (رد شد) · CRUD شعبه (موجود) · استفاده از ساعت شعبه در محاسبهٔ
اسلات (تسک ۰۳) · اتاق بهعنوان منبع قابل رزرو (تسک ۰۲).
Endpoint ها
| متد | مسیر | توضیح |
|---|---|---|
| GET | /api/v1/branch/{addressUuid}/working-hours |
ساعت کاری هفتگی یک شعبه |
| PUT | /api/v1/branch/{addressUuid}/working-hours |
جایگزینی کامل هفت روز |
| GET | /api/v1/branch/{addressUuid}/rooms |
اتاقهای شعبه |
| POST | /api/v1/room |
ساخت اتاق |
| PATCH | /api/v1/room/{uuid} |
ویرایش |
| DELETE | /api/v1/room/{uuid} |
حذف (فقط بدون منبع فعال — گارد در تسک ۰۲ تکمیل میشود) |
{addressUuid} همان uuid رکورد doctor_addresses است. «شعبه» و «آدرس» یک چیزند.
معیار پذیرش
- ✅ موفق: کلینیک
PUT /branch/{uuid}/working-hoursبا هفت روز میفرستد →200؛GETهمان ساختار را با کلیدهای0..6(۰=شنبه، همان قراردادSlotCalculatorService) برمیگرداند. - ✅ موفق:
POST /api/v1/roomباaddress_uuidوcapacity: 3→201، و جفت tenant اتاق از آدرس مشتق میشود نه از بدنهٔ درخواست. - ✅ موفق:
activeپیشفرضtrueوtimezoneپیشفرضAsia/Tehran— هیچ آدرس موجودی رفتارش عوض نمیشود. - ❌ خطا: آدرس محیط دیگر →
404(رفتارTenantFilter، نه ۴۰۳). - ❌ خطا: ساعت با
end_minute <= start_minute→422. - ❌ خطا: دو بازهٔ همپوشان در یک روز →
422. - ❌ خطا:
capacity <= 0→422. - ⚠️ مرزی: بازهٔ شبانهروزی
0..1440→200(یک ردیف، نه دو). - ⚠️ مرزی: روز بدون هیچ بازه → معتبر، یعنی شعبه آن روز بسته است.
- ⚠️ مرزی: شعبهٔ بدون هیچ ساعت کاری → «تعریفنشده»، نه «همیشهباز». تسک ۰۳ در این
حالت به رفتار فعلی برمیگردد (برنامهٔ پزشک تنها مرجع). این تصمیم باید در
docs/api/branch.mdنوشته شود. - ⚠️ مرزی:
PUTبا آرایهٔ خالی → همهٔ ساعتهای آن شعبه پاک میشوند (بستنِ کامل شعبه).
خروجی
src/Branch/—BranchWorkingHours،Room، سرویسها، کنترلرها- دو ستون روی
DoctorAddress+ migration assets/admin/pages/BranchWorkingHoursPage.tsx+BranchRoomsPage.tsxdocs/api/branch.md- checklist.md کاملشده