Files
clinicpro/docs/api/appointment-plan.md
T
hamedandClaude Opus 5 584ea4067f feat(policy): six-category policy engine wired into the booking flow
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>
2026-07-31 10:19:19 +03:30

151 lines
6.6 KiB
Markdown
Raw 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/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` را می‌گیرد. بدون این، هر سرویس موجود بی‌برنامه می‌شد.
**۲. بخش بدون هیچ نیازمندی معتبر است.** «انتظار در خانه» زمان می‌گیرد ولی هیچ منبعی
اشغال نمی‌کند.
**۳. قید جنسیت وقتی جنسیت بیمار نامشخص است نادیده گرفته نمی‌شود.** ۴۲۲ می‌دهد، چون رد
کردن بی‌صدا یعنی بیمار به منبعی می‌رسد که قرار نبود.
`mergeable` هم‌نام‌ها یک بار می‌آیند: «آماده‌سازی» با دو ناحیه یک بار انجام می‌شود.
تکرار شدنِ کارِ اصلی با `duration_source: "items"` بیان می‌شود، نه با تکرار بخش.
---
## طبقه‌بندی محیط
| جدول | وضعیت |
|---|---|
| `segment_templates` | جفت محیط (از بخشِ سرویس مشتق می‌شود) |
| `segment_requirements` | `AGGREGATE_CHILDREN` — ریشه `SegmentTemplate` |
## تست‌ها
```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)