Files
clinicpro/docs/new_feture/taskes/task-01-branch-room/task.md
T
hamedandClaude Opus 5 eebb363b9f feat(branch): branch working hours and rooms on the existing address entity
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>
2026-07-30 16:28:04 +03:30

6.1 KiB
Raw Blame History

تسک ۰۱ — شعبه و اتاق

فاز: ۱ (هسته) · وابستگی: ۰۰ · زمان: ۶-۸ ساعت (بازتخمین — رجوع به بخش «تصحیح طرح»)


تصحیح طرح (اجرای ۱۴۰۵/۰۵/۰۸)

نسخهٔ اول این تسک یک جدول 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_iddoctor_addresses.id
  • GET /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: 3201، و جفت tenant اتاق از آدرس مشتق می‌شود نه از بدنهٔ درخواست.
  • موفق: active پیش‌فرض true و timezone پیش‌فرض Asia/Tehran — هیچ آدرس موجودی رفتارش عوض نمی‌شود.
  • خطا: آدرس محیط دیگر → 404 (رفتار TenantFilter، نه ۴۰۳).
  • خطا: ساعت با end_minute <= start_minute422.
  • خطا: دو بازهٔ هم‌پوشان در یک روز → 422.
  • خطا: capacity <= 0422.
  • ⚠️ مرزی: بازهٔ شبانه‌روزی 0..1440200 (یک ردیف، نه دو).
  • ⚠️ مرزی: روز بدون هیچ بازه → معتبر، یعنی شعبه آن روز بسته است.
  • ⚠️ مرزی: شعبهٔ بدون هیچ ساعت کاری → «تعریف‌نشده»، نه «همیشه‌باز». تسک ۰۳ در این حالت به رفتار فعلی برمی‌گردد (برنامهٔ پزشک تنها مرجع). این تصمیم باید در docs/api/branch.md نوشته شود.
  • ⚠️ مرزی: PUT با آرایهٔ خالی → همهٔ ساعت‌های آن شعبه پاک می‌شوند (بستنِ کامل شعبه).

خروجی

  • src/Branch/BranchWorkingHours، Room، سرویس‌ها، کنترلرها
  • دو ستون روی DoctorAddress + migration
  • assets/admin/pages/BranchWorkingHoursPage.tsx + BranchRoomsPage.tsx
  • docs/api/branch.md
  • checklist.md کامل‌شده