Files
clinicpro/docs/new_feture/taskes/task-10-policy-admin-sandbox/architecture.md
T
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

7.4 KiB
Raw Blame History

معماری — تسک ۱۰

ساختار فایل

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);
}

تضمین «هیچ چیزی ثبت نمی‌شود»

سه لایه، نه یکی:

  1. evaluateIsolated() روی DTO کار می‌کند، نه entity — هیچ entity ای تغییر نمی‌کند
  2. کل شبیه‌سازی داخل تراکنشی اجرا می‌شود که همیشه rollback می‌شود:
    $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 — الگوهای آماده

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() مهم است: آزمایش نسخهٔ ۱ اجازهٔ فعال‌سازی نسخهٔ ۲ را نمی‌دهد.