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,161 @@
|
||||
# معماری — تسک ۱۰
|
||||
|
||||
## ساختار فایل
|
||||
|
||||
```
|
||||
src/Policy/
|
||||
├── Simulation/
|
||||
│ ├── PolicySimulator.php # اجرای dry-run
|
||||
│ ├── SimulationSampler.php # انتخاب نمونهٔ نوبتهای واقعی
|
||||
│ └── Dto/{SimulationReport, SimulationRow}.php
|
||||
├── Entity/PolicySimulationRun.php
|
||||
├── Template/PolicyTemplateRegistry.php
|
||||
└── Controller/PolicySimulationController.php
|
||||
|
||||
assets/admin/
|
||||
├── pages/PoliciesPage.tsx
|
||||
├── pages/PolicyFormPage.tsx
|
||||
├── pages/PolicySimulationPage.tsx
|
||||
└── components/PolicyConditionBuilder.tsx # از schema ساخته میشود
|
||||
```
|
||||
|
||||
## `PolicySimulator` — dry-run واقعی
|
||||
|
||||
```php
|
||||
public function simulate(Policy $policy, int $sampleSize = 50): SimulationReport
|
||||
{
|
||||
$sample = $this->sampler->recentAppointments($policy, $sampleSize);
|
||||
$rows = [];
|
||||
|
||||
foreach ($sample as $appointment) {
|
||||
$ctx = PolicyContext::fromAppointment($appointment);
|
||||
|
||||
$before = $this->snapshotOf($appointment); // وضعیت واقعی ثبتشده
|
||||
$after = $this->engineFor($policy->getCategory())
|
||||
->evaluateIsolated($policy, $ctx); // فقط همین قانون
|
||||
|
||||
if ($before->equals($after)) continue; // بیتأثیر
|
||||
$rows[] = new SimulationRow($appointment, $before, $after);
|
||||
}
|
||||
|
||||
return new SimulationReport($policy, count($sample), $rows);
|
||||
}
|
||||
```
|
||||
|
||||
### تضمین «هیچ چیزی ثبت نمیشود»
|
||||
|
||||
سه لایه، نه یکی:
|
||||
|
||||
1. `evaluateIsolated()` روی DTO کار میکند، نه entity — هیچ entity ای تغییر نمیکند
|
||||
2. کل شبیهسازی داخل تراکنشی اجرا میشود که **همیشه** rollback میشود:
|
||||
```php
|
||||
$this->em->beginTransaction();
|
||||
try { $report = $this->runInternal($policy, $size); }
|
||||
finally { $this->em->rollback(); $this->em->clear(); }
|
||||
```
|
||||
3. تست تعداد ردیفهای جدولهای حساس را قبل و بعد مقایسه میکند
|
||||
|
||||
لایهٔ ۲ حتی اگر کسی روزی سهواً یک `flush` اضافه کرد، جلویش را میگیرد. `em->clear()`
|
||||
اجباری است، وگرنه entity های کثیف در identity map میمانند و درخواست بعدی همان request
|
||||
آنها را flush میکند.
|
||||
|
||||
`PolicySimulationRun` **بعد از** rollback و در یک تراکنش جدا ثبت میشود.
|
||||
|
||||
## `evaluateIsolated` — چرا فقط همین قانون
|
||||
|
||||
شبیهسازی باید بگوید «**این** قانون چه تغییری میدهد»، نه «نتیجهٔ نهایی با همهٔ قوانین چه
|
||||
میشود». دومی مفید است ولی سؤال کاربر نیست: کاربر دارد یک قانون میسازد و میخواهد اثر
|
||||
همان را ببیند.
|
||||
|
||||
هر یک از شش موتور تسک ۰۹ باید یک متد `evaluateIsolated(Policy, PolicyContext)` داشته باشد
|
||||
که بدون `PolicyResolver` و بدون `Combiner` فقط همان قانون را ارزیابی کند.
|
||||
|
||||
## `PolicyTemplateRegistry` — الگوهای آماده
|
||||
|
||||
```php
|
||||
private const TEMPLATES = [
|
||||
'min_days_between_sessions' => [
|
||||
'title' => 'حداقل فاصله بین جلسات',
|
||||
'category' => 'spacing',
|
||||
'inputs' => [
|
||||
['key' => 'service_uuid', 'type' => 'service_select', 'label' => 'سرویس'],
|
||||
['key' => 'days', 'type' => 'int', 'label' => 'حداقل روز', 'min' => 1, 'max' => 365],
|
||||
],
|
||||
'build' => /* callable که conditions و effects را میسازد */,
|
||||
],
|
||||
'surgery_needs_surgeon' => […], // resource
|
||||
'vip_discount' => […], // pricing
|
||||
'minor_needs_consent' => […], // eligibility
|
||||
'complex_min_duration' => […], // timing
|
||||
];
|
||||
```
|
||||
|
||||
کاربر ۹۰٪ موارد از الگو استفاده میکند و هرگز شرط خام نمینویسد. حالت پیشرفته
|
||||
(`PolicyConditionBuilder`) برای بقیه است.
|
||||
|
||||
## فرم از schema، نه hard-code
|
||||
|
||||
```tsx
|
||||
// PolicyConditionBuilder.tsx
|
||||
const { data: schema } = useQuery({ queryKey: ['policy-schema'], queryFn: … });
|
||||
|
||||
// هر شرط: [فیلد ▾] [عملگر ▾] [مقدار]
|
||||
// - فیلدها از schema.fields
|
||||
// - عملگرهای مجاز از schema.fields[field].ops ← فیلتر میشود، نه همه
|
||||
// - نوع ورودی مقدار از schema.fields[field].type
|
||||
// int → عدد · array → SearchableSelect چندانتخابی · enum → SearchableSelect
|
||||
```
|
||||
|
||||
اگر عملگرها را فیلتر نکنی، کاربر `patient.tags > 5` میسازد و `422` میگیرد بدون فهمیدن چرا.
|
||||
|
||||
هرگز `<select>` بومی — `SearchableSelect` طبق قاعدهٔ پروژه.
|
||||
|
||||
## گزارش شبیهسازی — UI
|
||||
|
||||
```
|
||||
PolicySimulationPage.tsx
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ آزمایش قانون: حداقل ۲۱ روز فاصله بین جلسات لیزر │
|
||||
│ │
|
||||
│ نمونه: ۵۰ نوبت اخیر · تحت تأثیر: ۷ نوبت (۱۴٪) │
|
||||
│ ⚠️ شدت: متوسط │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ بیمار تاریخ نوبت وضعیت فعلی → با این قانون │
|
||||
│ ز. احمدی ۱۴۰۵/۰۴/۱۲ مجاز → رد میشد │
|
||||
│ م. کریمی ۱۴۰۵/۰۴/۱۵ مجاز → رد میشد │
|
||||
│ … │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
[بازگشت به ویرایش] [فعالسازی قانون]
|
||||
```
|
||||
|
||||
ستون «وضعیت فعلی → با این قانون» تنها چیزی است که کاربر غیرفنی میفهمد. درصد و شدت هم
|
||||
لازم است: قانونی که ۹۸٪ نوبتها را رد میکند تقریباً همیشه اشتباه نوشته شده.
|
||||
|
||||
سطح شدت:
|
||||
|
||||
| تحت تأثیر | شدت | رنگ |
|
||||
|---|---|---|
|
||||
| ۰٪ | `none` | خاکستری + هشدار «این قانون روی هیچ نوبتی اثر نداشت» |
|
||||
| ۱-۲۰٪ | `low` | سبز |
|
||||
| ۲۱-۶۰٪ | `medium` | نارنجی |
|
||||
| > ۶۰٪ | `high` | قرمز + متن «مطمئنید؟» روی دکمهٔ فعالسازی |
|
||||
|
||||
شدت `none` هم هشدار است: یعنی شرط احتمالاً هرگز true نمیشود.
|
||||
|
||||
## `activate` با شرط آزمایش
|
||||
|
||||
```php
|
||||
// PolicyService::activate()
|
||||
$run = $this->simulationRepo->latestFor($policy);
|
||||
if ($run === null || $run->getPolicyVersion() !== $policy->getVersion()) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
'ابتدا قانون را آزمایش کنید و نتیجه را ببینید',
|
||||
422
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
`getPolicyVersion() !== $policy->getVersion()` مهم است: آزمایش نسخهٔ ۱ اجازهٔ فعالسازی
|
||||
نسخهٔ ۲ را نمیدهد.
|
||||
Reference in New Issue
Block a user