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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# «شعبه» جدول تازهای نیست — `doctor_addresses` است
|
||||
|
||||
**این سند بر همهٔ تسکهایی که `branches` یا `branch_id` میگویند حاکم است.**
|
||||
نسخهٔ اول تسک ۰۱ یک جدول `branches` طراحی کرده بود؛ در اجرا معلوم شد آن موجودیت از قبل
|
||||
وجود دارد. تسک ۰۱ اصلاح شد و جدول ساخته **نشد**.
|
||||
|
||||
هر جا در تسکهای ۰۲، ۰۴، ۰۷، ۰۸، ۰۹، ۱۰، ۱۳ نوشته شده `branch_id INT NOT NULL FK →
|
||||
branches(id)`، بخوانید:
|
||||
|
||||
```sql
|
||||
address_id INT NOT NULL -- FK → doctor_addresses(id)
|
||||
```
|
||||
|
||||
و هر جا `Branch $branch` نوشته شده، بخوانید `DoctorAddress $address`.
|
||||
|
||||
---
|
||||
|
||||
## چرا
|
||||
|
||||
`App\Doctor\Entity\DoctorAddress` تمام چیزی است که یک شعبه لازم دارد:
|
||||
|
||||
| نیاز شعبه | در `DoctorAddress` |
|
||||
|---|---|
|
||||
| نام | `name` |
|
||||
| آدرس | `address` |
|
||||
| تلفن | `telephone` |
|
||||
| مختصات | `latitude` / `longitude` |
|
||||
| شهر و استان | FK به entity `City` / `Province` |
|
||||
| مالک (محیط) | `forDoctor(Doctor)` یا `forClinic(int $clinicId)` + ستون `type` |
|
||||
| فعال/غیرفعال | `active` — **تسک ۰۱ اضافه کرد** |
|
||||
| منطقهٔ زمانی | `timezone` — **تسک ۰۱ اضافه کرد** |
|
||||
|
||||
و از قبل در کل سیستم به همین معنا مصرف میشود:
|
||||
|
||||
- `WeeklySchedule.setting[day].sessions[].location_id` → `doctor_addresses.id`
|
||||
- `SlotCalculatorService::buildSessionSlots()` آن را در هر اسلات کپی میکند
|
||||
- `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`
|
||||
|
||||
ساختن جدول موازی یعنی دو منبع حقیقت برای نام/آدرس/تلفن/مختصات یک مکان فیزیکی، و شعبهای
|
||||
که `location_id` هرگز به آن اشاره نمیکند — یعنی decorative. قاعدهٔ #۸ پروژه:
|
||||
«API/جدول جدید فقط وقتی هیچ موجودی — حتی با توسعه — کافی نباشد.»
|
||||
|
||||
---
|
||||
|
||||
## پیامد برای تسکهای بعدی
|
||||
|
||||
| تسک | چه چیزی عوض میشود |
|
||||
|---|---|
|
||||
| ۰۲ منابع | `clinic_resources.address_id` → `doctor_addresses(id)`. `ResourcePool` هم همین. «منابع مال شعبهاند» = مال یک آدرساند |
|
||||
| ۰۳ تقویم منبع | ساعت کاری شعبه از `branch_working_hours` که به `doctor_addresses.id` کلید میخورد |
|
||||
| ۰۴ کاتالوگ | `service_branch_overrides.address_id` |
|
||||
| ۰۷ رزرو | `appointments.address_id` **از قبل وجود دارد** (`Appointment::$addressId`) — ستون جدید لازم نیست |
|
||||
| ۰۸ قیمت | `price_lists.address_id` |
|
||||
| ۰۹/۱۰ قوانین | `policies.address_id` (اختصاصیبودن per شعبه) |
|
||||
| ۱۳ لغو/انتظار | `waitlist_entries.address_id` |
|
||||
|
||||
⚠️ نکتهٔ تسک ۰۷: `Appointment` از قبل `address_id` دارد (ستون `addressId`, تهیپذیر) و
|
||||
`SlotCalculatorService::resolveSlotLocationId()` پرش میکند. پس آنجا هم ستون تازه لازم نیست.
|
||||
|
||||
---
|
||||
|
||||
## جفت tenant اتاق و منابع
|
||||
|
||||
`DoctorAddress` ستونهای `entity_type`/`entity_id` ندارد؛ مالکیتش با `type` +
|
||||
`doctor_id`/`clinic_id` بیان میشود. موجودیتهای جدیدی که به آدرس کلید میخورند و
|
||||
`TenantOwnedTrait` دارند، جفتشان را در **سازنده از آدرس مشتق** میکنند:
|
||||
|
||||
```php
|
||||
// App\Branch\Entity\Room::__construct()
|
||||
$this->assignTenantPair(
|
||||
$address->getType() === DoctorAddress::TYPE_CLINIC ? 'clinic' : 'doctor',
|
||||
$address->getType() === DoctorAddress::TYPE_CLINIC
|
||||
? (int) $address->getClinicId()
|
||||
: (int) $address->getDoctor()->getId(),
|
||||
);
|
||||
```
|
||||
|
||||
همان قاعدهٔ `docs/architecture/tenancy.md`: جفت در سازنده از ریشه مشتق میشود، نه از
|
||||
ورودی درخواست — پس هیچ نقطهٔ ساختی نمیتواند فراموشش کند و write-once میماند.
|
||||
Reference in New Issue
Block a user