- 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.
12 KiB
Appointment Plan API — بخشهای نوبت و سازندهٔ برنامه
Base:
/api/v1· Auth: JWT وابسته به resource.md (منبع و مهارت) و clinic-services.md (مدت آیتمها).
چرا نوبت یک تکه نیست
بند ۷ مستند. یک جلسهٔ لیزر:
| بخش | مدت | اتاق | اپراتور | دستگاه |
|---|---|---|---|---|
| مالیدن کرم بیحسی | ۵ | اشغال | اشغال | آزاد |
| انتظار اثر کرم | ۳۰ | اشغال | آزاد | آزاد |
| خود لیزر | ۲۰ | اشغال | اشغال | اشغال |
| مراقبت بعد | ۵ | اشغال | اشغال | آزاد |
با مدل تکبازهای، اپراتور ۶۰ دقیقه قفل میشود در حالی که ۳۰ دقیقه کار میکند — نصف ظرفیت هدر میرود.
⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمیکند. فقط شکل نوبت را میسازد؛ پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است.
مجوزها
از ۲۰۲۶-۰۸-۰۸:
GET /service-item/{uuid}/segments→services.viewPUT /service-item/{uuid}/segments→services.updatePOST /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
{
"service_uuid": "…لیزر",
"branch_uuid": "…شعبه",
"item_uuids": ["…صورت", "…بیکینی"],
"patient_gender": "female"
}
۲۰۰:
{
"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 |
الگوی نمونه
ddev exec php bin/console app:segment:seed-templates --service=<uuid> --preset=beauty
سه الگو: beauty (آمادهسازی · بیحسی · انتظار · کار اصلی · تمیزکاری) · dental
(معاینه · درمان · ضدعفونی یونیت) · physio (ارزیابی · جلسهٔ درمان · استراحت).
نقطهٔ شروع است نه پیکربندی نهایی: کلینیک از روی چیزی که میبیند ویرایش میکند، نه از روی
صفحهٔ خالی. روی سرویسی که از قبل بخش دارد کاری نمیکند مگر --force — بازنویسی
خاموشِ چیزی که کلینیک خودش ساخته، بدترین رفتار ممکن است. نقشی که آن محیط تعریف نکرده،
ساخته نمیشود و در خروجی گزارش میشود؛ نوع منبع تصمیم کلینیک است.
تستها
ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php # ۱۵ تست
اثر موتور قوانین
- دستهٔ
timing:min_duration_minutes(بیشترین برنده) وadd_duration_minutes(جمع) روی مجموع نوبت اعمال میشوند؛ رشدِ لازم به آخرین بخش میچسبد تا آفست بخشهای قبلی جابهجا نشود. - دستهٔ
resource: نقشی کهrequire_resourceمیخواهد، اگر هیچ بخشی نداشته باشد، به اولین بخشی که بیمار حاضر است اضافه میشود. نقش ناشناخته یا بیمنبع422میدهد، نه بیاثر ماندن. - هر دو روی سرویسِ بیالگو هم اجرا میشوند.
جزئیات: policy.md