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()` مهم است: آزمایش نسخهٔ ۱ اجازهٔ فعالسازی
|
||||
نسخهٔ ۲ را نمیدهد.
|
||||
@@ -0,0 +1,71 @@
|
||||
# دیتابیس — تسک ۱۰
|
||||
|
||||
## `policy_simulation_runs`
|
||||
|
||||
تنها جدول جدید این تسک — و تنها چیزی که شبیهسازی مینویسد.
|
||||
|
||||
| ستون | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| `id` | INT PK AI | |
|
||||
| `uuid` | VARCHAR(36) UNIQUE | |
|
||||
| `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | |
|
||||
| `policy_id` | INT NOT NULL | FK → `policies.id` ON DELETE CASCADE |
|
||||
| `policy_version` | SMALLINT NOT NULL | نسخهٔ آزمایششده |
|
||||
| `sample_size` | SMALLINT NOT NULL | تعداد نوبت نمونه |
|
||||
| `affected_count` | SMALLINT NOT NULL | تعداد تحت تأثیر |
|
||||
| `severity` | VARCHAR(10) NOT NULL | `none`\|`low`\|`medium`\|`high` |
|
||||
| `report` | JSON NOT NULL | ردیفهای تفصیلی (حداکثر ۵۰) |
|
||||
| `run_by` | INT NULL | FK → `users.id` ON DELETE SET NULL |
|
||||
| `created_at` | INT NOT NULL | |
|
||||
|
||||
```sql
|
||||
KEY idx_psr_policy (policy_id, policy_version, created_at)
|
||||
KEY idx_psr_tenant (entity_type, entity_id, created_at)
|
||||
```
|
||||
|
||||
`report` سقف حجم دارد: ۵۰ ردیف × چند فیلد ≈ چند کیلوبایت. بیشتر ذخیره نکن — گزارش
|
||||
تفصیلیتر با اجرای دوباره به دست میآید.
|
||||
|
||||
## هیچ تغییری در جدولهای دیگر
|
||||
|
||||
`policies.active` از قبل هست. شرط آزمایش در سطح سرویس اعمال میشود، نه schema.
|
||||
|
||||
## نمونهگیری — `SimulationSampler`
|
||||
|
||||
```sql
|
||||
-- نوبتهای واقعی، مرتبط با دامنهٔ قانون، جدیدترین اول
|
||||
SELECT a.* FROM appointments a
|
||||
WHERE a.entity_type = :type AND a.entity_id = :id
|
||||
AND a.status IN ('confirmed','completed')
|
||||
AND (:branchId IS NULL OR a.branch_id = :branchId)
|
||||
AND (:serviceId IS NULL OR a.service_item_id = :serviceId)
|
||||
ORDER BY a.slot_start DESC
|
||||
LIMIT 50
|
||||
```
|
||||
|
||||
فیلتر `service_item_id` از خودِ شرط قانون استخراج میشود (اگر قانون سرویس مشخصی را
|
||||
هدف گرفته). بدون آن، شبیهسازی قانون لیزر روی ۵۰ نوبت دندانپزشکی اجرا میشود و
|
||||
«۰٪ تحت تأثیر» میدهد — که گمراهکننده است.
|
||||
|
||||
## Migration
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console doctrine:migrations:diff --no-interaction
|
||||
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
## پاکسازی
|
||||
|
||||
اجراهای آزمایشی قدیمی ارزشی ندارند:
|
||||
|
||||
```bash
|
||||
ddev exec php bin/console app:policy:prune-simulations --older-than=30d --force
|
||||
```
|
||||
|
||||
آخرین اجرا per (policy, version) **هرگز** حذف نمیشود — چون شرط `activate` به آن وابسته است.
|
||||
|
||||
## طبقهبندی tenant
|
||||
|
||||
| جدول | وضعیت |
|
||||
|---|---|
|
||||
| `policy_simulation_runs` | جفت tenant |
|
||||
@@ -0,0 +1,130 @@
|
||||
# نکات پیادهسازی — تسک ۱۰
|
||||
|
||||
## ۱. rollback اجباری، سه لایه
|
||||
|
||||
```php
|
||||
public function simulate(Policy $policy, int $size): SimulationReport
|
||||
{
|
||||
$this->em->beginTransaction();
|
||||
try {
|
||||
return $this->runInternal($policy, $size);
|
||||
} finally {
|
||||
$this->em->rollback(); // ← حتی اگر استثنا پرت شود
|
||||
$this->em->clear(); // ← identity map پاک شود
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`finally` نه `catch`: اگر شبیهسازی استثنا داد، هنوز باید rollback شود.
|
||||
`clear()` بدون آن، entity های تغییرکردهٔ درون تراکنش در حافظه میمانند و اولین `flush`
|
||||
در ادامهٔ همان request آنها را ثبت میکند — یک باگ که پیدا کردنش روزها میبرد.
|
||||
|
||||
ثبت `PolicySimulationRun` **بعد** از این بلوک و در تراکنش خودش.
|
||||
|
||||
## ۲. تست «هیچ چیزی ننوشت» — با شمارش، نه با اعتماد
|
||||
|
||||
```php
|
||||
$before = $this->countRows(['appointments','price_snapshots','resource_occupancy',
|
||||
'resource_occupancy_slot','appointment_segments']);
|
||||
$this->simulator->simulate($policy, 50);
|
||||
$after = $this->countRows([...]);
|
||||
self::assertSame($before, $after, 'شبیهسازی نباید هیچ ردیفی بنویسد');
|
||||
```
|
||||
|
||||
این تست ارزشمندترین تست این تسک است. هر بار که کسی `PolicySimulator` را تغییر دهد،
|
||||
همین تست جلوی فاجعه را میگیرد.
|
||||
|
||||
## ۳. `evaluateIsolated` روی همهٔ شش موتور
|
||||
|
||||
اضافه کردن این متد به شش موتور تسک ۰۹، تغییر اینترفیس است. پس **در تسک ۰۹ اضافه شود**،
|
||||
نه اینجا — وگرنه شش کلاس دوباره ویرایش میشوند.
|
||||
|
||||
اگر تسک ۰۹ تمام شده و این متد نیست، اضافهاش کن ولی بهعنوان یک متد در همان اینترفیس
|
||||
موجود، نه یک اینترفیس جدید.
|
||||
|
||||
## ۴. فرم از schema — بدون استثنا
|
||||
|
||||
```tsx
|
||||
// ❌ اولین وسوسه
|
||||
const FIELDS = ['patient.age', 'patient.tags', 'service.category_path'];
|
||||
|
||||
// ✅
|
||||
const { data: schema } = useQuery({ queryKey: ['policy-schema'], staleTime: 300_000 });
|
||||
```
|
||||
|
||||
اگر فیلدها را در فرانت hard-code کنی، هر فیلد جدید در `FieldRegistry` نیاز به تغییر
|
||||
فرانت دارد و بعد از دو ماه دو فهرست ناهمگام داریم. `staleTime` بلند اشکالی ندارد —
|
||||
schema تقریباً هرگز عوض نمیشود.
|
||||
|
||||
## ۵. شدت `none` هم هشدار است
|
||||
|
||||
قانونی که روی هیچ نوبتی اثر نداشت، دو حالت دارد:
|
||||
- شرطش هرگز true نمیشود (اشتباه نوشته شده)
|
||||
- نمونهٔ ۵۰ نوبتی آن حالت را نداشت (شاید درست است)
|
||||
|
||||
پیام باید هر دو را بگوید:
|
||||
|
||||
> «این قانون روی هیچکدام از ۵۰ نوبت نمونه اثر نداشت. یا شرط آن هرگز برقرار نمیشود،
|
||||
> یا این حالت در نوبتهای اخیر پیش نیامده. فعالسازی مجاز است.»
|
||||
|
||||
فعالسازی را نبند — کلینیک جدید هیچ نوبتی ندارد و باید بتواند قانون بسازد.
|
||||
|
||||
## ۶. الگوها باید واقعاً کار کنند
|
||||
|
||||
هر الگو در `PolicyTemplateRegistry` باید یک تست داشته باشد که آن را میسازد،
|
||||
شبیهسازی میکند و فعال میکند. الگویی که `conditions` نامعتبر تولید کند، بدترین حالت است:
|
||||
کاربر فرم آماده را پر میکند و `422` میگیرد.
|
||||
|
||||
```php
|
||||
// tests/Policy/PolicyTemplateTest.php
|
||||
/** @dataProvider templates */
|
||||
public function testTemplateProducesValidPolicy(string $key): void
|
||||
{
|
||||
$policy = $this->registry->build($key, $this->sampleInputs($key));
|
||||
$this->validator->assertValid($policy); // همان اعتبارسنجی POST /policy
|
||||
}
|
||||
```
|
||||
|
||||
## ۷. edge case ها
|
||||
|
||||
| حالت | رفتار درست |
|
||||
|---|---|
|
||||
| محیط بدون هیچ نوبت | گزارش خالی، `severity=none`، `activate` مجاز |
|
||||
| قانون `deny` که همه را رد میکند | `severity=high`، فعالسازی با تأیید دوباره |
|
||||
| `simulate` نسخهٔ ۱، بعد نسخهٔ ۲ ساخته شد | `activate` نسخهٔ ۲ → `422` |
|
||||
| `simulate` دو بار پشتسرهم | آخری معیار است؛ قبلی میماند |
|
||||
| قانون فعال که دوباره `simulate` میشود | مجاز — کاربر میخواهد اثرش را ببیند |
|
||||
| نوبت نمونهای که سرویسش حذف شده | از نمونه حذف شود، در `sample_size` نیاید |
|
||||
| قانون `pricing` روی نوبتی بدون snapshot | آن ردیف رد شود با علت `no_snapshot` |
|
||||
| `sample_size` بزرگتر از ۵۰ | سقف ۵۰ — درخواست بیشتر `422` |
|
||||
|
||||
## ۸. تست
|
||||
|
||||
```
|
||||
tests/Policy/PolicySimulatorTest.php ← ⭐
|
||||
- هیچ ردیفی نوشته نمیشود (شمارش قبل/بعد)
|
||||
- رفتار درست وقتی evaluateIsolated استثنا میدهد (rollback + clear)
|
||||
- گزارش فقط نوبتهای تحت تأثیر را دارد
|
||||
tests/Policy/SimulationSamplerTest.php
|
||||
- فیلتر شعبه و سرویس از شرط قانون استخراج میشود
|
||||
- فقط confirmed/completed
|
||||
- سقف ۵۰
|
||||
tests/Policy/PolicyActivationGuardTest.php ← ⭐
|
||||
- activate بدون simulate → 422
|
||||
- activate با simulate نسخهٔ قبلی → 422
|
||||
- activate با simulate نسخهٔ جاری → 200
|
||||
- محیط بدون نوبت: simulate خالی → activate مجاز
|
||||
tests/Policy/PolicyTemplateTest.php
|
||||
- هر الگو قانون معتبر تولید میکند (dataProvider روی همهٔ الگوها)
|
||||
tests/Policy/SeverityTest.php
|
||||
- ۰٪ → none · ۱۰٪ → low · ۴۰٪ → medium · ۸۰٪ → high
|
||||
assets/admin/pages/PolicyFormPage.test.tsx
|
||||
- فیلدها از schema میآیند (mock schema با فیلد ساختگی → در UI ظاهر شود)
|
||||
- عملگرهای نامعتبر برای نوع فیلد نمایش داده نمیشوند
|
||||
```
|
||||
|
||||
## ۹. مستندات
|
||||
|
||||
`docs/api/policy.md` را با `simulate` و `policy-templates` و شرط جدید `activate`
|
||||
بهروز کن. در `docs/architecture/policy-engine.md` یک بخش «چرا آزمایش اجباری است»
|
||||
اضافه کن با ارجاع به ریسک دوم مستند بند ۱۷.
|
||||
@@ -0,0 +1,61 @@
|
||||
# تسک ۱۰ — فرم ساخت قانون و محیط آزمایش
|
||||
|
||||
**فاز:** ۲ (قوانین) · **وابستگی:** ۰۹ · **زمان:** ۱۰-۱۲ ساعت
|
||||
|
||||
---
|
||||
|
||||
## هدف
|
||||
|
||||
مستند بند ۱۷، ریسک دوم: «کاربر غیرفنی نمیتواند قانون درست تعریف کند → قانونهای اشتباه،
|
||||
رفتار عجیب». راهحل مستند: **فرم آماده، الگوهای از پیش تعریفشده، آزمایش اجباری قبل از
|
||||
فعال شدن.**
|
||||
|
||||
بدون این تسک، تسک ۰۹ یک API قدرتمند است که هیچکس نمیتواند از آن استفادهٔ درست کند.
|
||||
|
||||
## دامنه
|
||||
|
||||
**هست:**
|
||||
- فرم ساخت قانون که از `GET /api/v1/policy-schema` ساخته میشود (نه hard-code در فرانت)
|
||||
- الگوهای آماده (`policy templates`) — کاربر الگو را انتخاب و مقدار پر میکند
|
||||
- محیط آزمایش (`dry-run`): اجرای قانون روی داده واقعی بدون ثبت هیچ چیز
|
||||
- **آزمایش اجباری**: `activate` تا وقتی یک اجرای آزمایشی موفق ثبت نشده، رد میشود
|
||||
- نمایش تاریخچهٔ نسخهها با diff
|
||||
|
||||
**نیست:** موتور قانون (تسک ۰۹).
|
||||
|
||||
## Endpoint ها
|
||||
|
||||
| متد | مسیر | توضیح |
|
||||
|---|---|---|
|
||||
| POST | `/api/v1/policy/{uuid}/simulate` | اجرای آزمایشی روی نوبتهای واقعی گذشته |
|
||||
| GET | `/api/v1/policy-templates` | الگوهای آماده |
|
||||
|
||||
`POST /policy/{uuid}/activate` (تسک ۰۹) یک شرط جدید میگیرد: وجود یک `simulate` موفق
|
||||
برای نسخهٔ جاری.
|
||||
|
||||
## معیار پذیرش
|
||||
|
||||
- ✅ موفق: کاربر الگوی «حداقل فاصله بین جلسات» را انتخاب میکند، سرویس و تعداد روز را
|
||||
پر میکند، `simulate` میزند → گزارشی از ۵۰ نوبت اخیر: چند تا تحت تأثیر قرار میگرفتند و
|
||||
دقیقاً چه تغییری میکردند.
|
||||
- ✅ موفق: `simulate` هیچ ردیفی در دیتابیس نمینویسد (بهجز `policy_simulation_runs`).
|
||||
تست باید تعداد ردیفهای `appointments`, `price_snapshots`, `resource_occupancy` را
|
||||
قبل و بعد مقایسه کند.
|
||||
- ✅ موفق: بعد از `simulate` موفق، `activate` کار میکند.
|
||||
- ✅ موفق: فرم ساخت قانون بدون هیچ تغییر کد فرانت، فیلد جدیدی که به `FieldRegistry`
|
||||
اضافه شود را نشان میدهد.
|
||||
- ❌ خطا: `activate` بدون `simulate` → `422` با پیام «ابتدا قانون را آزمایش کنید».
|
||||
- ❌ خطا: `activate` بعد از تغییر محتوای قانون (نسخهٔ جدید) → `simulate` قبلی معتبر نیست
|
||||
→ `422`.
|
||||
- ⚠️ مرزی: محیطی که هیچ نوبت گذشتهای ندارد → `simulate` با گزارش خالی و
|
||||
`warning: 'دادهای برای آزمایش نیست'` موفق شود (وگرنه کلینیک جدید هرگز نمیتواند
|
||||
قانون فعال کند).
|
||||
- ⚠️ مرزی: قانون `deny` که همهٔ ۵۰ نوبت را رد میکند → `simulate` موفق ولی با
|
||||
`severity: 'high'` و پیام «این قانون همهٔ نوبتهای نمونه را رد میکند».
|
||||
- ⚠️ مرزی: `simulate` روی قانون دستهٔ `pricing` → تفاوت مبلغ per نوبت نمایش داده شود.
|
||||
|
||||
## خروجی
|
||||
|
||||
- `src/Policy/Simulation/`
|
||||
- `assets/admin/pages/PoliciesPage.tsx` + `PolicyFormPage.tsx` + `PolicySimulationPage.tsx`
|
||||
- `docs/api/policy.md` بهروزرسانی
|
||||
Reference in New Issue
Block a user