Files
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

162 lines
7.4 KiB
Markdown
Raw Permalink 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/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()` مهم است: آزمایش نسخهٔ ۱ اجازهٔ فعال‌سازی
نسخهٔ ۲ را نمی‌دهد.