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.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# معماری — تسک ۰۵
|
||||
|
||||
## ساختار فایل
|
||||
|
||||
```
|
||||
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`)، تعداد، استخر، مهارتها (چیپ)،
|
||||
نوع اشغال (رادیو با توضیح فارسی هر گزینه)
|
||||
- **نوار پیشنمایش زمانی**: چهار بخش روی یک خط با عرض متناسب مدت و آیکن منابع هر بخش
|
||||
— این تنها راهی است که کاربر غیرفنی میفهمد چه ساخته
|
||||
Reference in New Issue
Block a user