feat(branch): admin UI for branch working hours and rooms, plus real API docs
Three pages, all on the existing design system: BranchesPage lists the current environment's booking locations with their working-hours and active-room counts, and two subpages edit the week and the rooms. The list page deliberately does not create or rename a branch — clinic and doctor detail pages already do that, and duplicating it would give one physical place two edit surfaces. Route permission reuses `appointment_settings` rather than inventing a new one. Two real bugs fell out of exercising this end to end: `days` was serialising as a JSON *array*, not an object keyed "0".."6" — keys 0..6 are sequential so json_encode collapses them to a list. The client reads days["0"] either way, so nothing looked broken, but the response shape was unstable: one missing day would flip the same field to an object. The controller now casts to stdClass and WorkingHoursTest::testDaysIsAJsonObjectNotAnArray pins it. Found by curling the endpoint for the docs, not by any test. `<input type="time">` caps at 23:59, so it can neither display nor produce the legal end value 1440. An all-day range would have vanished from the form and been corrupted by the first save. Ranges now carry an explicit end-of-day flag, with a round-trip test proving 1440 survives. docs/api/branch.md documents all eight endpoints with responses captured from real curl runs against ddev, including the 422 and 404 bodies. doctor.md records that active/timezone now appear on all nine existing address endpoints (additive), and tenancy.md gains the two lessons this task taught: an aggregate child whose root is itself declared global inherits no environment and needs a real pair, and TenantFilter is not a substitute for an explicit ownership check because hard isolation only applies to a *chosen* context. Verified: phpunit 1067 tests / 2974 assertions green; slot-mode frozen contract green; phpstan 14 errors before and after, none in touched files; tsc clean; vitest 87 files / 612 tests green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
| ستون | مقدار |
|
||||
|---|---|
|
||||
| `entity_type` | `doctor` یا `clinic` — `VARCHAR(10)` در هر ۲۰ جدول tenant-دار |
|
||||
| `entity_type` | `doctor` یا `clinic` — `VARCHAR(10)` در همهٔ جدولهای tenant-دار (طولِ یکسان، وگرنه JOIN به collation mismatch میخورد) |
|
||||
| `entity_id` | شناسهٔ همان پزشک یا کلینیک |
|
||||
|
||||
موجودیتها این جفت را از trait مشترک میگیرند:
|
||||
@@ -132,6 +132,28 @@ public function __construct(ServiceSection $section, ...) {
|
||||
|
||||
`RequestReachableChildTenantTest` این را **بدون هیچ گارد دستی** میسنجد: فقط خودِ فیلتر. با برداشتن ستون، همان نشتی مالی فاز ۷ برمیگردد و تست قرمز میشود.
|
||||
|
||||
### ⚠️ ریشهٔ سراسری، فرزندِ محیطدار — پروندهٔ `doctor_addresses`
|
||||
|
||||
`doctor_addresses` عمداً در `ENTITIES` سراسری است («آدرسهای پزشک؛ در همهٔ محیطهای او
|
||||
یکسان است»)، پس **فیلتر رویش اعمال نمیشود** و `findOneBy(['uuid' => …])` آدرس کلینیک
|
||||
دیگر را هم برمیگرداند. دو جدولِ تسک شعبه روی همین ریشه نشستند و درس دادند:
|
||||
|
||||
| جدول | طبقهبندی | چرا |
|
||||
|---|---|---|
|
||||
| `branch_working_hours` | جفت محیط **خودش** | اول بهعنوان فرزند aggregate با ریشهٔ `DoctorAddress` ثبت شد و `TenantSchemaCoverageTest` ردش کرد: ریشهای که خودش سراسری است، هیچ محیطی برای ارث دادن ندارد |
|
||||
| `rooms` | جفت محیط خودش | uuidش از درخواست میآید — همان قاعدهٔ فاز ۸ |
|
||||
|
||||
جفت از **`type` آدرس** مشتق میشود، که نگاشتی کامل است:
|
||||
`personal ⇒ (doctor, doctor_id)` و `clinic ⇒ (clinic, clinic_id)`. چون آدرس هم فقط در
|
||||
محیط خودش فهرست میشود، هیچ ردیفی بیدلیل پنهان نمیشود.
|
||||
|
||||
خودِ آدرس محافظ دستی دارد: `App\Branch\Service\BranchResolver` تکنقطهٔ تبدیل
|
||||
«uuid شعبه در request» به آدرسِ محیط جاری است و در غیر این صورت **۴۰۴** میدهد — همان
|
||||
رفتار فیلتر، نه ۴۰۳.
|
||||
|
||||
**درسِ عملیاتی:** آنجا که `AGGREGATE_CHILDREN` بیفایده است، فقط طبقهبندی عوض نکن؛
|
||||
جفت واقعی بده. و برای ریشهٔ سراسری یک resolver واحد بساز، نه بررسی تکراری در هر کنترلر.
|
||||
|
||||
### uuid از درخواست — خطرناکترین الگو
|
||||
|
||||
سه نشتی واقعی در آدیت این نقطه پیدا شد و **هیچکدام در repository نبودند**؛ همه در کنترلر و سرویس بودند، جایی که یک uuid از بدنه یا کوئری میآید و کسی محیطش را نمیسنجد:
|
||||
@@ -152,6 +174,13 @@ $this->tenantOwnership->allBelongTo($context, $entities); // یک بی
|
||||
|
||||
موجودیتی که جفتش را expose نکند، **استثنا میدهد** — سکوت اینجا گاردِ همیشه-بسته میسازد که خودش باگ است.
|
||||
|
||||
**فیلتر جایگزین این بررسی نیست، حتی روی جدولِ جفتدار.** جداسازی سختِ `TenantFilter`
|
||||
فقط روی محیطِ **انتخابشده** اعمال میشود ({@see `EntityContext::$chosen`}). پزشکی که
|
||||
هنوز محیطی برنگزیده در هیچ محیطی «نیست»، پس فیلتر برایش خاموش است و
|
||||
`PATCH /api/v1/room/{uuid}` میتوانست اتاق کلینیک دیگری را ویرایش کند — با
|
||||
`RoomCrudTest::testForeignRoomIsNotFound` گرفته شد که قبل از اصلاح ۲۰۰ میداد.
|
||||
هر کنترلری که uuid را از request میگیرد باید `belongsToPair()` را خودش صدا بزند.
|
||||
|
||||
`TenantLookupInventoryTest` تعداد این جستوجوها را per-file نگه میدارد. افزودن یک `findByUuid` تازه روی موجودیت محیطدار تست را قرمز میکند تا کسی ثابت کند محیطش بررسی میشود و بعد عدد را بهروز کند.
|
||||
|
||||
### جدولهای مالی
|
||||
|
||||
Reference in New Issue
Block a user