- 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.
197 lines
9.3 KiB
Markdown
197 lines
9.3 KiB
Markdown
# معماری — تسک ۰۵
|
||
|
||
## ساختار فایل
|
||
|
||
```
|
||
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`)، تعداد، استخر، مهارتها (چیپ)،
|
||
نوع اشغال (رادیو با توضیح فارسی هر گزینه)
|
||
- **نوار پیشنمایش زمانی**: چهار بخش روی یک خط با عرض متناسب مدت و آیکن منابع هر بخش
|
||
— این تنها راهی است که کاربر غیرفنی میفهمد چه ساخته
|