Files
clinicpro/docs/api/appointment-plan.md
hamed 934405c42d feat: Implement permission gate for appointment and billing controllers
- 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.
2026-08-08 10:27:13 +03:30

222 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)