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:
@@ -1,9 +1,10 @@
|
||||
# چکلیست — تسک ۰۱ (شعبه و اتاق)
|
||||
|
||||
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** 🔄 در حال انجام · **آخرین بازبینی:** —
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md) ·
|
||||
[branch-is-doctor-address.md](../_shared/branch-is-doctor-address.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -13,80 +14,97 @@
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | `SlotCalculatorService` دستنخورده | ⏳ | این تسک به آن کاری ندارد |
|
||||
| ۰.۳ | `location_id` در JSON برنامهٔ هفتگی دستنخورده | ⏳ | شعبه **بالای** آدرس مینشیند |
|
||||
| ۰.۴ | `DoctorAddress` هیچ ستونی حذف/تغییر نداد | ⏳ | فقط `branch_id` تهیپذیر اضافه شد |
|
||||
| ۰.۳ | `location_id` در JSON برنامهٔ هفتگی دستنخورده | ⏳ | شعبه = همان `doctor_addresses.id` |
|
||||
| ۰.۴ | `DoctorAddress` هیچ ستونی حذف/تغییر نداد | ⏳ | فقط `active` و `timezone` با `DEFAULT` |
|
||||
| ۰.۵ | `active=false` هیچ اثری بر محاسبهٔ اسلات ندارد | ⏳ | اعمالش تسک ۰۳ است |
|
||||
|
||||
## ۱. بکاند
|
||||
## ۱. طرح
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۱.۱ | `Branch` + `BranchWorkingHours` + `Room` entity | ⏳ | |
|
||||
| ۱.۲ | `BranchService` با گاردهای حذف قابل توسعه (`DeletionGuardInterface`) | ⏳ | تسک ۰۲ و ۰۷ گارد اضافه میکنند |
|
||||
| ۱.۳ | `WorkingHoursService` — اعتبارسنجی و `sequence` سمت سرور | ⏳ | |
|
||||
| ۱.۴ | ساعت با `start_minute`/`end_minute` عددی، نه رشتهٔ `"09:00"` | ⏳ | |
|
||||
| ۱.۵ | هشت endpoint ساخته شد | ⏳ | |
|
||||
| ۱.۶ | پزشک مستقل هم شعبه دارد (مطب = شعبه) | ⏳ | نه فقط `entity_type=clinic` |
|
||||
| ۱.۷ | `app:branch:backfill` — dry-run پیشفرض، idempotent | ⏳ | |
|
||||
| ۱.۸ | کنترلر نازک · `BaseController` · `success/paginated/error` | ⏳ | |
|
||||
| ۱.۹ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
|
||||
| ۱.۱ | ⛔ جدول `branches` **ساخته نشد** — دلیل مکتوب | ✅ | `_shared/branch-is-doctor-address.md` |
|
||||
| ۱.۲ | `task.md` · `architecture.md` · `database.md` · `implementation_notes.md` تصحیح شد | ✅ | |
|
||||
| ۱.۳ | ارجاعهای `branch_id` در تسکهای ۰۲/۰۴/۰۷/۰۸/۰۹/۱۰/۱۳ با سند حاکم پوشش داده شد | ✅ | ۲۴ ارجاع — یک سند واحد در `_shared` بهجای ویرایش ۲۴ نقطه |
|
||||
|
||||
## ۲. دیتابیس
|
||||
## ۲. بکاند
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `branches` · `branch_working_hours` · `rooms` | ⏳ | |
|
||||
| ۲.۲ | `entity_type, entity_id` ستون **اول** ایندکسهای لیست | ⏳ | |
|
||||
| ۲.۳ | `timezone` روی شعبه از روز اول | ⏳ | افزودن بعدی = backfill زماندار |
|
||||
| ۲.۴ | `rooms.capacity` — ظرفیت همزمان | ⏳ | اتاق سهتخته = یک ردیف با ۳ |
|
||||
| ۲.۵ | `branch_working_hours` در `GlobalTables::AGGREGATE_CHILDREN` | ⏳ | |
|
||||
| ۲.۶ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۲.۱ | `DoctorAddress` += `active` + `timezone` | ⏳ | |
|
||||
| ۲.۲ | `timezone` با `DateTimeZone::listIdentifiers()` اعتبارسنجی میشود، نه regex | ⏳ | |
|
||||
| ۲.۳ | `BranchWorkingHours` entity (فرزند aggregate) | ⏳ | |
|
||||
| ۲.۴ | `Room` entity با `TenantOwnedTrait` و جفت مشتق از آدرس در سازنده | ⏳ | نه از بدنهٔ request |
|
||||
| ۲.۵ | `BranchResolver` — تکنقطهٔ uuid آدرس → محیط جاری، ۴۰۴ نه ۴۰۳ | ⏳ | `TenantFilter` روی `doctor_addresses` کار نمیکند |
|
||||
| ۲.۶ | `WorkingHoursService` — اعتبارسنجی کامل **قبل از** حذف (اتمی) | ⏳ | |
|
||||
| ۲.۷ | ساعت با `start_minute`/`end_minute` عددی، نه رشتهٔ `"09:00"` | ⏳ | |
|
||||
| ۲.۸ | `sequence` سمت سرور تخصیص مییابد، نه کلاینت | ⏳ | |
|
||||
| ۲.۹ | `RoomService` با گارد حذف قابل توسعه (آرایهٔ تزریقی، نه زنجیرهٔ `if`) | ⏳ | تسک ۰۲ و ۰۷ گارد اضافه میکنند |
|
||||
| ۲.۱۰ | شش endpoint ساخته شد | ⏳ | صفر endpoint CRUD شعبه — موجود است |
|
||||
| ۲.۱۱ | پزشک مستقل هم شعبه دارد | ⏳ | `type='personal'` از قبل کار میکند |
|
||||
| ۲.۱۲ | کنترلر نازک · `BaseController` · `success/paginated/error` | ⏳ | |
|
||||
|
||||
## ۳. UI
|
||||
## ۳. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `BranchesPage` · `BranchFormPage` · `RoomsPage` | ⏳ | |
|
||||
| ۳.۲ | `DataTable` با skeleton و empty state فارسی | ⏳ | |
|
||||
| ۳.۳ | `PageHeader` با `backTo` روی زیرصفحهها | ⏳ | |
|
||||
| ۳.۴ | شهر/استان با `SearchableSelect` — هیچ `<select>` بومی | ⏳ | |
|
||||
| ۳.۵ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
|
||||
| ۳.۶ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن | ⏳ | |
|
||||
| ۳.۷ | دارکمود و حالت فشرده بررسی شد | ⏳ | |
|
||||
| ۳.۸ | RTL و موبایل بررسی شد | ⏳ | |
|
||||
| ۳.۹ | همهٔ رشتهها فارسی از i18n | ⏳ | |
|
||||
| ۳.۱۰ | هشدار UI: «هیچ شعبهٔ فعالی باقی نمیماند» | ⏳ | |
|
||||
| ۳.۱۱ | مسیرها در `App.tsx` | ⏳ | |
|
||||
| ۳.۱ | `branch_working_hours` · `rooms` | ⏳ | |
|
||||
| ۳.۲ | `entity_type, entity_id` ستون **اول** ایندکس `rooms` | ⏳ | |
|
||||
| ۳.۳ | `timezone` روی آدرس از روز اول | ⏳ | افزودن بعدی = backfill زماندار |
|
||||
| ۳.۴ | `rooms.capacity` — ظرفیت همزمان | ⏳ | اتاق سهتخته = یک ردیف با ۳ |
|
||||
| ۳.۵ | `branch_working_hours` در `GlobalTables::AGGREGATE_CHILDREN` با ریشهٔ صریح | ⏳ | |
|
||||
| ۳.۶ | ستونها و جدولها روی `db_test` هم ساخته شد | ⏳ | تاریخچهٔ migration جدا |
|
||||
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۳.۸ | `TenantLookupInventoryTest` سبز — repository جدید ثبت شد | ⏳ | |
|
||||
|
||||
## ۴. تست
|
||||
## ۴. UI
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `BranchCrudTest` — نقشها، ۴۰۴ نه ۴۰۳ برای محیط دیگر | ⏳ | |
|
||||
| ۴.۲ | `WorkingHoursTest` — `end<=start`، همپوشانی، شبانهروزی `0..1440` | ⏳ | |
|
||||
| ۴.۳ | `BranchDeletionTest` — شعبهٔ دارای اتاق فعال → ۴۲۲ | ⏳ | |
|
||||
| ۴.۴ | `capacity=0` → ۴۲۲ | ⏳ | |
|
||||
| ۴.۵ | `phpstan analyse src/Branch` بدون خطا | ⏳ | |
|
||||
| ۴.۱ | `BranchesPage` · `BranchWorkingHoursPage` · `BranchRoomsPage` | ⏳ | |
|
||||
| ۴.۲ | `DataTable` با skeleton و empty state فارسی | ⏳ | |
|
||||
| ۴.۳ | `PageHeader` با `backTo` روی زیرصفحهها | ⏳ | |
|
||||
| ۴.۴ | هر `select` با `SearchableSelect` — هیچ `<select>` بومی | ⏳ | |
|
||||
| ۴.۵ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
|
||||
| ۴.۶ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن | ⏳ | |
|
||||
| ۴.۷ | دارکمود و حالت فشرده بررسی شد | ⏳ | |
|
||||
| ۴.۸ | RTL و موبایل بررسی شد | ⏳ | |
|
||||
| ۴.۹ | هشدار UI: «هیچ شعبهٔ فعالی باقی نمیماند» | ⏳ | |
|
||||
| ۴.۱۰ | مسیرها در `App.tsx` + ورودی در `SettingsMenuPage` | ⏳ | |
|
||||
| ۴.۱۱ | مجوز موجود `appointment_settings` استفاده شد، نه مجوز تازه | ⏳ | |
|
||||
|
||||
## ۵. مستندات
|
||||
## ۵. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | `docs/api/branch.md` + ثبت در `docs/api/README.md` | ⏳ | |
|
||||
| ۵.۲ | تفسیر «شعبهٔ بدون ساعت کاری = تعریفنشده، نه همیشهباز» نوشته شد | ⏳ | تسک ۰۳ رویش حساب میکند |
|
||||
| ۵.۳ | `docs/architecture/tenancy.md` جدول طبقهبندی بهروز شد | ⏳ | |
|
||||
| ۵.۱ | `WorkingHoursTest` — هفت روز، `end<=start`، همپوشانی، `0..1440`، آرایهٔ خالی | ⏳ | |
|
||||
| ۵.۲ | اتمی بودن: بازهٔ نامعتبر در روز ششم → ۴۲۲ و شش روز قبلی دستنخورده | ⏳ | |
|
||||
| ۵.۳ | `RoomCrudTest` — جفت tenant مشتق، `capacity=0` → ۴۲۲ | ⏳ | |
|
||||
| ۵.۴ | آدرس/اتاق محیط دیگر → ۴۰۴ (نه ۴۰۳) | ⏳ | |
|
||||
| ۵.۵ | `BranchAddressFieldsTest` — پیشفرضها، `timezone` نامعتبر → ۴۲۲ | ⏳ | |
|
||||
| ۵.۶ | `phpstan analyse src/Branch` بدون خطا | ⏳ | |
|
||||
|
||||
## ۶. بازبینی پایانی
|
||||
## ۶. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
|
||||
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۶.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
|
||||
| ۶.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۶.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۶.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ⏳ | این تسک قرارداد عمومی عوض نمیکند |
|
||||
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
|
||||
| ۶.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۶.۱ | `docs/api/branch.md` + ثبت در `docs/api/README.md` | ⏳ | JSON واقعی از curl |
|
||||
| ۶.۲ | «شعبهٔ بدون ساعت کاری = تعریفنشده، نه همیشهباز» نوشته شد | ⏳ | تسک ۰۳ رویش حساب میکند |
|
||||
| ۶.۳ | «`active` در این فاز بیاثر بر اسلات» نوشته شد | ⏳ | |
|
||||
| ۶.۴ | `docs/api/doctor.md` — دو فیلد جدید در پاسخ ۹ endpoint آدرس | ⏳ | تغییر قرارداد است |
|
||||
| ۶.۵ | `docs/architecture/tenancy.md` جدول طبقهبندی بهروز شد | ⏳ | |
|
||||
|
||||
## ۷. بازبینی پایانی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۷.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۷.۴ | `phpstan` بدون خطای جدید (مقایسه با کامیت پیش از تسک) | ⏳ | |
|
||||
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۷.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
|
||||
| ۷.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۷.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۷.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ⏳ | دو فیلد جدید additive است |
|
||||
| ۷.۱۰ | commit، سپس `graphify update .`، سپس commit جدا | ⏳ | |
|
||||
| ۷.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
|
||||
Reference in New Issue
Block a user