- 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.
7.4 KiB
معماری — تسک ۱۰
ساختار فایل
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 واقعی
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);
}
تضمین «هیچ چیزی ثبت نمیشود»
سه لایه، نه یکی:
evaluateIsolated()روی DTO کار میکند، نه entity — هیچ entity ای تغییر نمیکند- کل شبیهسازی داخل تراکنشی اجرا میشود که همیشه rollback میشود:
$this->em->beginTransaction(); try { $report = $this->runInternal($policy, $size); } finally { $this->em->rollback(); $this->em->clear(); } - تست تعداد ردیفهای جدولهای حساس را قبل و بعد مقایسه میکند
لایهٔ ۲ حتی اگر کسی روزی سهواً یک flush اضافه کرد، جلویش را میگیرد. em->clear()
اجباری است، وگرنه entity های کثیف در identity map میمانند و درخواست بعدی همان request
آنها را flush میکند.
PolicySimulationRun بعد از rollback و در یک تراکنش جدا ثبت میشود.
evaluateIsolated — چرا فقط همین قانون
شبیهسازی باید بگوید «این قانون چه تغییری میدهد»، نه «نتیجهٔ نهایی با همهٔ قوانین چه میشود». دومی مفید است ولی سؤال کاربر نیست: کاربر دارد یک قانون میسازد و میخواهد اثر همان را ببیند.
هر یک از شش موتور تسک ۰۹ باید یک متد evaluateIsolated(Policy, PolicyContext) داشته باشد
که بدون PolicyResolver و بدون Combiner فقط همان قانون را ارزیابی کند.
PolicyTemplateRegistry — الگوهای آماده
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
// 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 با شرط آزمایش
// 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() مهم است: آزمایش نسخهٔ ۱ اجازهٔ فعالسازی
نسخهٔ ۲ را نمیدهد.