# معماری — تسک ۱۰ ## ساختار فایل ``` 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` می‌گیرد بدون فهمیدن چرا. هرگز `