- Added PermissionGateTrait to manage access control for AppointmentPlanController and BillingController. - Introduced denyUnlessGrantedForPlanning method in AppointmentPlanController to handle specific permission checks for planning appointments. - Updated existing methods in both controllers to utilize the new permission checks. - Refactored ResourcePermissionTrait to use PermissionGateTrait for cleaner permission management. - Added tests to ensure proper permission enforcement across different scenarios, including cross-tenant access restrictions for staff.
222 lines
12 KiB
Markdown
222 lines
12 KiB
Markdown
# Appointment Plan API — بخشهای نوبت و سازندهٔ برنامه
|
||
|
||
> **Base:** `/api/v1` · **Auth:** JWT
|
||
> وابسته به [resource.md](resource.md) (منبع و مهارت) و [clinic-services.md](clinic-services.md) (مدت آیتمها).
|
||
|
||
---
|
||
|
||
## چرا نوبت یک تکه نیست
|
||
|
||
بند ۷ مستند. یک جلسهٔ لیزر:
|
||
|
||
| بخش | مدت | اتاق | اپراتور | دستگاه |
|
||
|---|---|---|---|---|
|
||
| مالیدن کرم بیحسی | ۵ | اشغال | اشغال | آزاد |
|
||
| انتظار اثر کرم | ۳۰ | اشغال | **آزاد** | آزاد |
|
||
| خود لیزر | ۲۰ | اشغال | اشغال | اشغال |
|
||
| مراقبت بعد | ۵ | اشغال | اشغال | آزاد |
|
||
|
||
با مدل تکبازهای، اپراتور ۶۰ دقیقه قفل میشود در حالی که ۳۰ دقیقه کار میکند —
|
||
**نصف ظرفیت هدر میرود**.
|
||
|
||
⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمیکند. فقط **شکل** نوبت را میسازد؛
|
||
پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است.
|
||
|
||
---
|
||
|
||
## مجوزها
|
||
|
||
از ۲۰۲۶-۰۸-۰۸:
|
||
|
||
- `GET /service-item/{uuid}/segments` → `services.view`
|
||
- `PUT /service-item/{uuid}/segments` → `services.update`
|
||
- `POST /appointment-plan/preview` → `services.view` **یا** `appointments.view`
|
||
|
||
منبعش `services` است نه `appointments`: بخشبندی یک خاصیتِ `ServiceItem` است و صفحهاش
|
||
داخل کاتالوگ خدمات مینشیند. همان استدلالِ پروتکل درمان در آدیت ۲۰۲۶-۰۸-۰۷.
|
||
|
||
`preview` استثناست و «یا» میگیرد، چون ورودیِ فرمِ ثبت نوبت است نه پیکربندیِ سرویس:
|
||
منشیای که اجازهٔ ثبت نوبت دارد ولی کاتالوگ خدمات برایش بسته است، وگرنه نمیتوانست
|
||
همان نوبتی را که مجاز است ثبت کند. قرینهٔ `ResourcePermissionTrait::denyUnlessGrantedForBooking`.
|
||
|
||
> **چرا اضافه شد:** این کنترلر دقیقاً همان شکلِ `TreatmentProtocolController` پیش از
|
||
> رفعِ یافتهٔ ۱ آدیت را داشت — `#[IsGranted('IS_AUTHENTICATED_FULLY')]` سطحکلاس و یک
|
||
> `requireItem()` که فقط مالکیتِ محیط را میسنجد. مالکیت مجوز نیست: عبور از آن فقط
|
||
> ثابت میکند سرویس مالِ همین محیط است، نه اینکه این کاربر حق دستزدن به آن را دارد.
|
||
|
||
---
|
||
|
||
## `GET/PUT /api/v1/service-item/{uuid}/segments`
|
||
|
||
`PUT` جایگزینی کامل است. هر بخش:
|
||
|
||
| فیلد | نوع | توضیح |
|
||
|---|---|---|
|
||
| `sequence` | int | ترتیب اجرا |
|
||
| `name` | string | ✅ الزامی |
|
||
| `duration_source` | `fixed` \| `items` | پیشفرض `fixed` |
|
||
| `duration_minutes` | int | برای `fixed` باید مثبت باشد |
|
||
| `patient_present` | bool | پیشفرض `true`؛ «انتظار در خانه» خلافش است |
|
||
| `mergeable` | bool | با چند آیتم **یک بار** میآید |
|
||
| `requirements` | array | نیازمندی منبع |
|
||
|
||
**`duration_source: "items"`** یعنی مدت این بخش از آیتمهای انتخابشده میآید و با
|
||
فرمول تسک ۰۴ حساب میشود. «خود لیزر» با دو ناحیه طولانیتر میشود، ولی «انتظار اثر
|
||
کرم» نه — به همین دلیل دو منبع مدت لازم است و یک عدد ثابت کافی نیست.
|
||
|
||
هر نیازمندی:
|
||
|
||
| فیلد | توضیح |
|
||
|---|---|
|
||
| `type_uuid` | ✅ نوع منبع (اتاق، اپراتور، دستگاه) |
|
||
| `skill_uuid` | مهارت لازم |
|
||
| `count` | پیشفرض ۱ |
|
||
| `occupancy` | `exclusive` (قفل کامل) یا `shared` (ظرفیت میشمارد) |
|
||
| `constraints` | فعلاً فقط `same_gender_as_patient` |
|
||
|
||
**۴۲۲:** نام خالی · بخش `fixed` با مدت صفر · مجموع بیش از ۴۸۰ دقیقه · `occupancy` یا
|
||
`constraint` ناشناخته.
|
||
|
||
---
|
||
|
||
## `POST /api/v1/appointment-plan/preview`
|
||
|
||
```json
|
||
{
|
||
"service_uuid": "…لیزر",
|
||
"branch_uuid": "…شعبه",
|
||
"item_uuids": ["…صورت", "…بیکینی"],
|
||
"patient_gender": "female"
|
||
}
|
||
```
|
||
|
||
**۲۰۰:**
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"total_minutes": 60,
|
||
"segments": [
|
||
{ "sequence": 1, "name": "بیحسی موضعی", "offset_minutes": 0, "duration_minutes": 5,
|
||
"patient_present": true, "mergeable": false,
|
||
"requirements": [
|
||
{ "role": "room", "role_name": "اتاق", "skill_name": null, "count": 1,
|
||
"occupancy": "exclusive", "constraints": [], "candidates": 3,
|
||
"occupancy_offset": { "setup_minutes": 0, "cleanup_minutes": 0 } }
|
||
] }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
`candidates` تعداد منابع واجد شرایط است. `occupancy_offset` در نمای کاربر نمایش داده
|
||
**نمیشود**؛ برای موتور جستجوی وقت است و محافظهکارانه از بیشترین مقدارِ کاندیدها
|
||
گرفته میشود — کم گرفتنش یعنی نوبت بعدی روی زمان تمیزکاری بیفتد.
|
||
|
||
### خطاها
|
||
|
||
| کد | کِی |
|
||
|---|---|
|
||
| `ERR_NO_ELIGIBLE_RESOURCE` (۴۲۲) | هیچ منبعی شرایط یک بخش را ندارد |
|
||
| `ERR_VALIDATION_002` (۴۲۲) | قید جنسیت هست ولی جنسیت بیمار نامشخص است |
|
||
| `ERR_VALIDATION_001` (۴۲۲) | مدت بخش تعیین نشده · مجموع بیش از سقف |
|
||
| ۴۰۴ | سرویس یا شعبهٔ محیط دیگر |
|
||
|
||
پیام `ERR_NO_ELIGIBLE_RESOURCE` انسانی است و نقش، مهارت و شعبه را میگوید:
|
||
«هیچ اپراتور خانمی با مهارت «لیزر آلکساندرایت» در شعبهٔ «مرکزی» موجود نیست» (بند ۱۰).
|
||
|
||
---
|
||
|
||
## سه قرارداد
|
||
|
||
**۱. سرویس بدون الگوی بخش، همان رفتار امروز را میگیرد.** یک بخش پیوسته به اندازهٔ کل
|
||
مدت که منبعِ `type=doctor` را میگیرد. بدون این، هر سرویس موجود بیبرنامه میشد.
|
||
|
||
**۲. بخش بدون هیچ نیازمندی معتبر است.** «انتظار در خانه» زمان میگیرد ولی هیچ منبعی
|
||
اشغال نمیکند.
|
||
|
||
**۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمیشود.** ۴۲۲ میدهد، چون رد
|
||
کردن بیصدا یعنی بیمار به منبعی میرسد که قرار نبود.
|
||
|
||
## سقفها و ایمنی جایگزینی
|
||
|
||
| قید | مقدار | چرا |
|
||
|---|---|---|
|
||
| مجموع مدت | ۴۸۰ دقیقه | حفاظت از جستجوی وقت |
|
||
| تعداد بخش | ۲۰ | موتور برای هر بخش × هر نیازمندی ترکیب منابع را میسنجد |
|
||
| نیازمندی هر بخش | ۱۰ | همان |
|
||
|
||
`PUT .../segments` **حذفکن-و-بنویس** است. همهٔ اعتبارسنجیها (قید ناشناخته، نوع اشغال،
|
||
مدت، سقفها) **پیش از حذف** انجام میشوند و خودِ حذف و نوشتن در یک تراکنشاند: خطای بعد
|
||
از حذف یعنی سرویس بدون بخش میماند و نوبتدهیاش بیصدا به «یک بخش پیوسته» برمیگردد —
|
||
که مدت و منابع همهٔ نوبتهای بعدی را عوض میکند.
|
||
|
||
پاسخ `preview` علاوه بر `total_minutes`، فیلد `patient_facing_minutes` هم دارد: مدتی که
|
||
بیمار واقعاً روی صندلی است. نوبت نودقیقهای که چهل دقیقهاش انتظار اثر بیحسی است، «نود
|
||
دقیقه وقت بگذارید» نیست — و محاسبه یکجا در بکاند است تا هر کلاینت خودش جمع نزند.
|
||
|
||
## ادغام بخشها
|
||
|
||
برنامه از الگوهای **سرویس اصلی بهعلاوهٔ آیتمهای انتخابشده** ساخته میشود. تا پیش از
|
||
این فقط الگوهای سرویس اصلی خوانده میشد، پس بخشهایی که کلینیک روی خودِ ناحیه تعریف کرده
|
||
بود بیصدا نادیده میماند و `mergeable` هرگز کاری نمیکرد — در یک سرویس، دو بخشِ همنام
|
||
معنا ندارد.
|
||
|
||
| قاعده | چرا |
|
||
|---|---|
|
||
| همنامهای `mergeable` یک بار میآیند | «آمادهسازی» برای دو ناحیه یک بار انجام میشود |
|
||
| از میان همنامها **طولانیترین** میماند | آمادهسازی دو ناحیه کوتاهتر از طولانیترینشان نیست |
|
||
| `duration_source: "items"` هم یک بار میآید | `DurationCalculator` از قبل مجموع همهٔ آیتمها را داده؛ تکرارش یعنی دوبار شمردن |
|
||
| تعداد منبع پس از ادغام **بیشینه** است | دو ناحیه با هم دو اتاق نمیخواهند، ولی اگر یکی دو اپراتور لازم داشت ادغام نباید به یک تنزلش بدهد |
|
||
|
||
مثال: «لیزر صورت» با آمادهسازی ۵ دقیقه و «لیزر بیکینی» با آمادهسازی ۱۲ دقیقه، هر دو
|
||
انتخاب شوند → یک آمادهسازیِ ۱۲ دقیقهای، و کارِ اصلی به اندازهٔ مجموع دو ناحیه.
|
||
|
||
**برنامه قطعی است:** دو `preview` با همان ورودی خروجیِ بایتبهبایت یکسان میدهند (تست
|
||
دارد). برنامهای که بین پیشنمایش و رزرو جابهجا شود یعنی کاربر چیزی را تأیید کرده که
|
||
رزرو نشد.
|
||
|
||
---
|
||
|
||
## طبقهبندی محیط
|
||
|
||
| جدول | وضعیت |
|
||
|---|---|
|
||
| `segment_templates` | جفت محیط (از بخشِ سرویس مشتق میشود) |
|
||
| `segment_requirements` | `AGGREGATE_CHILDREN` — ریشه `SegmentTemplate` |
|
||
|
||
## الگوی نمونه
|
||
|
||
```bash
|
||
ddev exec php bin/console app:segment:seed-templates --service=<uuid> --preset=beauty
|
||
```
|
||
|
||
سه الگو: `beauty` (آمادهسازی · بیحسی · انتظار · کار اصلی · تمیزکاری) · `dental`
|
||
(معاینه · درمان · ضدعفونی یونیت) · `physio` (ارزیابی · جلسهٔ درمان · استراحت).
|
||
|
||
نقطهٔ شروع است نه پیکربندی نهایی: کلینیک از روی چیزی که میبیند ویرایش میکند، نه از روی
|
||
صفحهٔ خالی. روی سرویسی که از قبل بخش دارد **کاری نمیکند** مگر `--force` — بازنویسی
|
||
خاموشِ چیزی که کلینیک خودش ساخته، بدترین رفتار ممکن است. نقشی که آن محیط تعریف نکرده،
|
||
ساخته نمیشود و در خروجی گزارش میشود؛ نوع منبع تصمیم کلینیک است.
|
||
|
||
## تستها
|
||
|
||
```bash
|
||
ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php # ۱۵ تست
|
||
```
|
||
|
||
---
|
||
|
||
## اثر موتور قوانین
|
||
|
||
- دستهٔ `timing`: `min_duration_minutes` (بیشترین برنده) و `add_duration_minutes` (جمع)
|
||
روی **مجموع** نوبت اعمال میشوند؛ رشدِ لازم به **آخرین** بخش میچسبد تا آفست بخشهای
|
||
قبلی جابهجا نشود.
|
||
- دستهٔ `resource`: نقشی که `require_resource` میخواهد، اگر هیچ بخشی نداشته باشد، به
|
||
**اولین بخشی که بیمار حاضر است** اضافه میشود. نقش ناشناخته یا بیمنبع `422` میدهد، نه
|
||
بیاثر ماندن.
|
||
- هر دو روی سرویسِ **بیالگو** هم اجرا میشوند.
|
||
|
||
جزئیات: [policy.md](policy.md)
|