Files
clinicpro/docs/new_feture/taskes/task-05-appointment-plan/architecture.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
2026-07-30 11:43:58 +03:30

197 lines
9.3 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.
# معماری — تسک ۰۵
## ساختار فایل
```
src/Appointment/Plan/
├── Entity/
│ ├── SegmentTemplate.php
│ └── SegmentRequirement.php
├── Dto/
│ ├── AppointmentPlan.php # نتیجهٔ نهایی — immutable
│ ├── PlannedSegment.php
│ └── PlannedRequirement.php
├── Service/
│ ├── AppointmentPlanBuilder.php # ارکستراتور
│ ├── SegmentAssembler.php # جمع‌آوری + ادغام بخش‌ها
│ ├── SegmentDurationResolver.php# مدت هر بخش
│ └── RequirementResolver.php # نیازمندی → منابع کاندید
├── Controller/
│ ├── SegmentTemplateController.php
│ └── AppointmentPlanController.php
└── Exception/NoEligibleResourceException.php
```
پنج کلاس سرویس به‌جای یک کلاس بزرگ، چون هر کدام یک دلیل تغییر دارد: ادغام بخش‌ها،
محاسبهٔ مدت، و پیدا کردن منبع کاندید سه مسئلهٔ مستقل‌اند و تسک ۰۹ فقط به دوتای اول
قلاب می‌زند.
## `SegmentTemplate`
```php
class SegmentTemplate
{
use TenantOwnedTrait;
public const OWNER_SERVICE = 'service'; // بخش پایهٔ سرویس
public const OWNER_OPTION = 'option'; // بخش اضافهٔ یک آیتم
private string $ownerType;
private ?ServiceItem $serviceItem = null;
private ?ServiceOption $option = null;
private string $name; // «انتظار اثر بی‌حسی»
private string $segmentType; // کلید ادغام: prep | wait | treatment | aftercare | custom:*
private int $sequence; // ترتیب اجرا
private ?int $fixedMinutes = null; // مدت ثابت؛ null یعنی مدت پویا
private ?int $durationShare = null; // درصد از مدت محاسبه‌شدهٔ سرویس، وقتی fixedMinutes نیست
private bool $patientPresent = true;
private bool $mergeable = false; // با بخش‌های هم‌نوع ادغام می‌شود
private bool $active = true;
}
```
### مدت ثابت یا سهمی
دو حالت، دقیقاً یکی از آن‌ها:
- `fixedMinutes = 30` — انتظار اثر کرم همیشه ۳۰ دقیقه است، چه یک ناحیه چه پنج ناحیه
- `durationShare = 100` — «خود لیزر» همهٔ مدتِ محاسبه‌شده از `DurationCalculator` (تسک ۰۴)
را می‌گیرد
جمع `durationShare` همهٔ بخش‌های یک سرویس باید دقیقاً ۱۰۰ باشد (اگر هیچ بخش سهمی نباشد،
شرط بی‌اثر است). اعتبارسنجی هنگام ذخیرهٔ الگو، نه هنگام ساخت برنامه.
## `SegmentRequirement`
```php
class SegmentRequirement
{
public const OCCUPANCY_EXCLUSIVE = 'exclusive'; // منبع کامل اشغال
public const OCCUPANCY_SHARED = 'shared'; // یک واحد از ظرفیت
public const OCCUPANCY_PASSIVE = 'passive'; // رزرو ولی بدون کار فعال
private SegmentTemplate $segment;
private ResourceType $role; // نقش: اپراتور، دستگاه، اتاق
private int $count = 1;
private ?ResourcePool $pool = null; // «هر عضو این استخر»
private ?ClinicResource $specific = null; // منبع مشخص (کم‌کاربرد ولی لازم)
private array $requiredSkills = []; // skill_id[] — همه لازم‌اند، نه یکی
private array $constraints = []; // {same_gender_as_patient: true, attributes: {...}}
private string $occupancy = self::OCCUPANCY_EXCLUSIVE;
}
```
`pool` و `specific` هر دو تهی‌پذیرند؛ اگر هیچ‌کدام نباشد یعنی «هر منبعِ آن نقش در آن شعبه
که شرط‌ها را دارد».
### `constraints` — فهرست بسته
مثل `DiscountRule`، شرط‌ها از یک فهرست بسته می‌آیند، نه کد دلخواه:
| کلید | مقدار | معنی |
|---|---|---|
| `same_gender_as_patient` | bool | منبع باید `attributes.gender` برابر جنسیت بیمار داشته باشد |
| `attributes` | object اسکالر | تطبیق دقیق روی `clinic_resources.attributes` |
| `min_skill_level` | 1..5 | حداقل سطح مهارت |
هر کلید ناشناخته → `422` هنگام ذخیره. این محدودیت عمدی است (مستند بند ۸): تسک ۰۶ باید
همهٔ این‌ها را به یک کوئری تبدیل کند.
## `AppointmentPlanBuilder` — جریان
```php
public function build(PlanRequest $request): AppointmentPlan
{
// ۱. اعتبارسنجی انتخاب (تسک ۰۴) — اگر نامعتبر بود همین‌جا تمام
$selection = $this->selectionValidator->validate(...);
// ۲. جمع‌آوری بخش‌ها: پایهٔ سرویس + بخش‌های اضافهٔ هر آیتم انتخابی
$raw = $this->assembler->collect($service, $selection->options);
// ۳. ادغام هم‌نوع‌ها (mergeable=true و segmentType یکسان → یکی)
$merged = $this->assembler->merge($raw);
// ۴. مدت هر بخش
$timed = $this->durationResolver->resolve($merged, $selection->totalMinutes);
// ۵. چیدمان: offset تجمعی بر اساس sequence
$sequenced = $this->assembler->layout($timed);
// ۶. نیازمندی‌ها → منابع کاندید (اینجا کوئری می‌خورد)
$withResources = $this->requirementResolver->resolve($sequenced, $request->branch, $request->patient);
// ۷. نقطهٔ اتصال تسک ۰۹: قوانین دستهٔ «منبع» و «زمان» اینجا اعمال می‌شوند
// فعلاً یک no-op PolicyApplier تزریق شود تا امضا بعداً عوض نشود.
return $this->policies->applyToPlan($withResources);
}
```
مرحلهٔ ۷ عمداً از روز اول در امضا هست حتی وقتی خالی است — افزودنش بعداً یعنی تغییر
امضای عمومی و همهٔ تست‌ها.
## ادغام بخش‌ها
```
ورودی: بخش‌های سرویس + بخش‌های همهٔ آیتم‌های انتخابی
گروه‌بندی بر اساس segmentType
برای هر گروه:
اگر همهٔ اعضا mergeable=true → یک بخش با:
نام: نام بخشِ سرویس (یا اولین)
مدت: بیشترین مدت ثابت، یا مجموع سهم‌ها
نیازمندی‌ها: اتحاد (بدون تکرار؛ count بیشینه برای هر نقش)
وگرنه → همه جدا می‌مانند، به ترتیب sequence
```
مثال مستند: بیمار پنج ناحیه انتخاب می‌کند، هر ناحیه یک بخش «آماده‌سازی» با
`mergeable=true` دارد → یک آماده‌سازی، نه پنج تا.
## `RequirementResolver`
```php
/** @return ClinicResource[] منابع واجد شرایط برای این نیازمندی */
public function candidates(SegmentRequirement $req, Branch $branch, ?Patient $patient): array
```
از `ClinicResourceRepository::findEligible()` (تسک ۰۲) استفاده می‌کند:
شعبه + نقش + `HAVING COUNT(DISTINCT skill) = n` + فیلتر `attributes` در PHP
(JSON در MariaDB قابل ایندکس‌گذاری مطمئن نیست؛ تعداد منابع یک شعبه کوچک است).
اگر `candidates === []``NoEligibleResourceException` با پیام ساخته‌شده از نقش و مهارت‌ها:
```php
throw new NoEligibleResourceException(sprintf(
'هیچ %s با مهارت %s در شعبهٔ %s موجود نیست',
$req->getRole()->getName(), implode('، ', $skillNames), $branch->getName()
));
```
پیام انسانی اجباری است — مستند بند ۱۰ صریح می‌گوید خطا باید بگوید چه چیزی کم است.
## سازگاری: سرویس بدون الگو
`SegmentAssembler::collect()` وقتی هیچ `SegmentTemplate` پیدا نکرد، یک بخش مجازی می‌سازد:
```php
new PlannedSegment(
name: $service->getName(),
segmentType: 'treatment',
durationMinutes: $selection->totalMinutes,
requirements: [ PlannedRequirement::doctorDefault() ], // منبع type=doctor
);
```
این تضمین می‌کند حالت `booking_mode=service` امروزی، وقتی به موتور جدید مهاجرت کند،
دقیقاً همان رفتار را داشته باشد.
## پنل ادمین
`ServiceSegmentsPage.tsx` (زیرصفحهٔ `ServiceDetailPage`):
- لیست مرتب بخش‌ها با drag ندارد؛ `sequence` عددی
- هر بخش قابل بازشدن: مدت (ثابت/سهمی)، حضور بیمار، ادغام‌پذیر
- زیر هر بخش، نیازمندی‌ها: نقش (`SearchableSelect`)، تعداد، استخر، مهارت‌ها (چیپ)،
نوع اشغال (رادیو با توضیح فارسی هر گزینه)
- **نوار پیش‌نمایش زمانی**: چهار بخش روی یک خط با عرض متناسب مدت و آیکن منابع هر بخش
— این تنها راهی است که کاربر غیرفنی می‌فهمد چه ساخته