Rules become data instead of code: a clinic can say "laser under 18 requires parental consent" without a deploy. Engine - Policy / PolicyVersionLog entities, closed field/operator/effect lists per category (PolicySchema), condition validation at write time - PolicyResolver: priority -> specificity -> age, combining effects by veto / max / sum / union - A missing fact fails its clause instead of silently passing it - Policies are drafts until activated, and are versioned rather than edited Wiring - selection -> ServiceSelectionValidator - eligibility + spacing -> BookingPolicyGuard, at hold time not confirm time - resource + timing -> AppointmentPlanBuilder, including template-less services - pricing -> PricingEngine, alongside (not replacing) the manual discount The condition column is named condition_json: `condition` is a MariaDB keyword and broke every INSERT. Tests: 17 in tests/Policy including NoPolicyRegressionTest, which pins that a clinic with no policies sees byte-identical output to task 08. Docs: docs/api/policy.md (real captured JSON) + docs/architecture/policy-engine.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.6 KiB
Appointment Plan API — بخشهای نوبت و سازندهٔ برنامه
Base:
/api/v1· Auth: JWT وابسته به resource.md (منبع و مهارت) و clinic-services.md (مدت آیتمها).
چرا نوبت یک تکه نیست
بند ۷ مستند. یک جلسهٔ لیزر:
| بخش | مدت | اتاق | اپراتور | دستگاه |
|---|---|---|---|---|
| مالیدن کرم بیحسی | ۵ | اشغال | اشغال | آزاد |
| انتظار اثر کرم | ۳۰ | اشغال | آزاد | آزاد |
| خود لیزر | ۲۰ | اشغال | اشغال | اشغال |
| مراقبت بعد | ۵ | اشغال | اشغال | آزاد |
با مدل تکبازهای، اپراتور ۶۰ دقیقه قفل میشود در حالی که ۳۰ دقیقه کار میکند — نصف ظرفیت هدر میرود.
⚠️ این بخش هیچ زمان مطلقی و هیچ منبع مشخصی تعیین نمیکند. فقط شکل نوبت را میسازد؛ پیدا کردن وقت و منبع آزاد کارِ تسک بعدی است.
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 را میگیرد. بدون این، هر سرویس موجود بیبرنامه میشد.
۲. بخش بدون هیچ نیازمندی معتبر است. «انتظار در خانه» زمان میگیرد ولی هیچ منبعی اشغال نمیکند.
۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمیشود. ۴۲۲ میدهد، چون رد کردن بیصدا یعنی بیمار به منبعی میرسد که قرار نبود.
mergeable همنامها یک بار میآیند: «آمادهسازی» با دو ناحیه یک بار انجام میشود.
تکرار شدنِ کارِ اصلی با duration_source: "items" بیان میشود، نه با تکرار بخش.
طبقهبندی محیط
| جدول | وضعیت |
|---|---|
segment_templates |
جفت محیط (از بخشِ سرویس مشتق میشود) |
segment_requirements |
AGGREGATE_CHILDREN — ریشه SegmentTemplate |
تستها
ddev exec php bin/phpunit tests/Appointment/AppointmentPlanTest.php # ۱۱ تست
اثر موتور قوانین
- دستهٔ
timing:min_duration_minutes(بیشترین برنده) وadd_duration_minutes(جمع) روی مجموع نوبت اعمال میشوند؛ رشدِ لازم به آخرین بخش میچسبد تا آفست بخشهای قبلی جابهجا نشود. - دستهٔ
resource: نقشی کهrequire_resourceمیخواهد، اگر هیچ بخشی نداشته باشد، به اولین بخشی که بیمار حاضر است اضافه میشود. نقش ناشناخته یا بیمنبع422میدهد، نه بیاثر ماندن. - هر دو روی سرویسِ بیالگو هم اجرا میشوند.
جزئیات: policy.md