feat(policy): rule builder and mandatory dry-run sandbox
Task 09 shipped a powerful API that a non-technical clinic owner could not safely use. This closes that gap: activation now requires having seen what the rule actually does. - PolicySimulator runs a policy against real past appointments and writes nothing: evaluation works on facts (never entities), the whole run sits in a transaction rolled back and cleared in `finally`, and a test counts rows in five sensitive tables before and after - activate() now demands a simulation of the *same version* — a report for version 1 does not unlock version 2 - PolicyTemplateRegistry: six ready-made rules, so the common case never touches a raw condition - Severity from the affected ratio; 0% is a warning too, since a rule that changes nothing usually has a condition that never matches - An empty clinic still succeeds with a warning, otherwise a new clinic could never activate anything Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a field that exists only in the schema shows up with no frontend change, and that operators are filtered per field type. The schema response now carries per-field metadata (label, type, meaningful operators) so the form has one source of truth instead of two. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+136
-2
@@ -41,15 +41,29 @@
|
||||
|
||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||
|
||||
هر دسته `label` فارسی، فهرست `fields`، همهٔ `operators`، `field_meta` (فراداده per فیلد
|
||||
شامل **عملگرهای معنادار برای همان نوع**) و `effects` با برچسب و نوع مقدار دارد.
|
||||
|
||||
> فرم باید عملگرها را از `field_meta[].operators` بخواند نه از `operators` کلی؛ وگرنه
|
||||
> کاربر `patient_tags > 5` میسازد و `422` میگیرد بدون اینکه بفهمد چرا.
|
||||
|
||||
### Response `200`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"selection": {
|
||||
"label": "انتخاب خدمات",
|
||||
"fields": ["item_count", "item_uuids", "catalog_category"],
|
||||
"operators": ["equals", "not_equals", "greater_than", "less_than", "in", "contains"],
|
||||
"effects": [{ "type": "forbid", "combination": "veto" }]
|
||||
"field_meta": [
|
||||
{ "label": "تعداد موارد انتخابی", "type": "int", "key": "item_count",
|
||||
"operators": ["equals", "not_equals", "greater_than", "less_than"] },
|
||||
{ "label": "موارد انتخابی", "type": "list", "key": "item_uuids", "operators": ["contains"] },
|
||||
{ "label": "دستهٔ کاتالوگ", "type": "uuid", "key": "catalog_category",
|
||||
"operators": ["equals", "not_equals", "in"] }
|
||||
],
|
||||
"effects": [{ "label": "ممنوع کن", "value_type": "none", "type": "forbid", "combination": "veto" }]
|
||||
},
|
||||
"eligibility": {
|
||||
"fields": ["patient_age", "patient_gender", "patient_tags", "has_parental_consent", "visit_count"],
|
||||
@@ -124,7 +138,9 @@
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `category` | string | ✅ | یکی از شش دسته |
|
||||
| `template` | string | — | کلید یک الگو از `GET /policy-templates`؛ اگر بیاید، `category`/`condition`/`effects` از الگو ساخته میشوند |
|
||||
| `values` | object | — | مقادیر ورودیهای همان الگو |
|
||||
| `category` | string | ✅ (بدون `template`) | یکی از شش دسته |
|
||||
| `name` | string | ✅ | نام قابلفهم؛ در پیام ممنوعیت به کاربر نشان داده میشود |
|
||||
| `condition` | object | — | `{match: "all"\|"any", conditions: [...]}` — خالی یعنی «همیشه» |
|
||||
| `condition.conditions[].field` | string | ✅ | باید در فهرست فیلدهای همان دسته باشد |
|
||||
@@ -275,3 +291,121 @@
|
||||
| `POST /api/v1/appointment-plan/preview` | `total_minutes` با `min_duration_minutes`/`add_duration_minutes` بزرگ میشود؛ نقشِ `require_resource` به اولین بخشِ حضور بیمار اضافه میشود |
|
||||
| `POST /api/v1/appointment-hold` | `422` وقتی قانون `eligibility` بیمار را رد کند، پرچم لازم نیامده باشد، یا فاصلهٔ `spacing` رعایت نشده باشد |
|
||||
| `POST /api/v1/pricing/quote` | تخفیف قوانین `pricing` به تخفیف دستی **اضافه** میشود و در `breakdown.sources.applied_policies` با `uuid`، `name` و `version` ثبت میشود |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## آزمایشگاه قانون (تسک ۱۰)
|
||||
|
||||
### GET `/api/v1/policy-templates`
|
||||
|
||||
الگوهای آماده. کاربر الگو را انتخاب میکند و فقط چند مقدار پر میکند؛ `condition` و
|
||||
`effects` سمت سرور ساخته میشوند و از همان اعتبارسنجی عادی رد میشوند.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"key": "vip_discount",
|
||||
"title": "تخفیف بیمار وفادار",
|
||||
"description": "بیمارانی که بیش از N ویزیت داشتهاند، درصدی تخفیف بگیرند.",
|
||||
"category": "pricing",
|
||||
"inputs": [
|
||||
{ "key": "visit_count", "type": "int", "label": "بیشتر از چند ویزیت", "min": 1, "max": 100 },
|
||||
{ "key": "percent", "type": "int", "label": "درصد تخفیف", "min": 1, "max": 100 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
ساخت قانون با الگو:
|
||||
|
||||
```json
|
||||
POST /api/v1/policy
|
||||
{ "name": "تخفیف مشتری وفادار", "template": "vip_discount", "values": { "visit_count": 3, "percent": 15 } }
|
||||
```
|
||||
|
||||
الگوهای موجود: `min_days_between_sessions` · `complex_min_duration` ·
|
||||
`extra_time_for_many_items` · `surgery_needs_surgeon` · `minor_needs_consent` ·
|
||||
`vip_discount`.
|
||||
|
||||
### POST `/api/v1/policy/{uuid}/simulate`
|
||||
|
||||
اجرای قانون روی نوبتهای واقعیِ گذشته، **بدون نوشتن هیچ چیز** جز خودِ نتیجه.
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `sample_size` | int | — | پیشفرض ۵۰، سقف ۲۰۰ |
|
||||
|
||||
نمونه به دامنهٔ خود قانون محدود میشود (شعبه/سرویس/دسته)، وگرنه «۰٪ تحت تأثیر» فقط
|
||||
یعنی نمونه اشتباه بوده.
|
||||
|
||||
#### Response `201`
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"uuid": "…",
|
||||
"policy_uuid": "…",
|
||||
"policy_version": 1,
|
||||
"sample_size": 4,
|
||||
"affected_count": 3,
|
||||
"affected_percent": 75,
|
||||
"severity": "high",
|
||||
"created_at": 1785480121,
|
||||
"rows": [
|
||||
{
|
||||
"appointment_uuid": "…",
|
||||
"patient_name": "ز. احمدی",
|
||||
"slot_start": 1785000000,
|
||||
"before": "2,000,000 ریال",
|
||||
"after": "1,500,000 ریال",
|
||||
"reason": "500,000 ریال تخفیف"
|
||||
}
|
||||
],
|
||||
"warning": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| شدت | نسبت تحت تأثیر | معنی |
|
||||
|---|---|---|
|
||||
| `none` | ۰٪ | **هشدار** — شرط احتمالاً هرگز برقرار نمیشود |
|
||||
| `low` | ۱–۲۰٪ | اثر محدود |
|
||||
| `medium` | ۲۱–۶۰٪ | بخش قابلتوجه |
|
||||
| `high` | > ۶۰٪ | بیشتر نوبتها؛ احتمالاً اشتباه نوشته شده |
|
||||
|
||||
محیطی که هیچ نوبت گذشتهای ندارد `201` میگیرد با `sample_size: 0`, `severity: "none"` و
|
||||
`warning: "دادهای برای آزمایش نیست"` — وگرنه کلینیک تازه هرگز نمیتوانست قانونی فعال کند.
|
||||
|
||||
### GET `/api/v1/policy/{uuid}/simulations`
|
||||
|
||||
ده اجرای آخر، جدیدترین اول.
|
||||
|
||||
### شرط تازهٔ `activate`
|
||||
|
||||
`POST /api/v1/policy/{uuid}/activate` حالا یک اجرای آزمایشیِ **همین نسخه** لازم دارد:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"errors": [{
|
||||
"code": "ERR_VALIDATION_001",
|
||||
"message": "ابتدا قانون را آزمایش کنید و نتیجه را ببینید",
|
||||
"field": "simulation"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
آزمایش نسخهٔ ۱ اجازهٔ فعالسازی نسخهٔ ۲ را نمیدهد.
|
||||
|
||||
### صفحههای پنل
|
||||
|
||||
| مسیر | صفحه |
|
||||
|---|---|
|
||||
| `/admin/policies` | فهرست قوانین |
|
||||
| `/admin/policies/new` | ساخت با الگو یا حالت پیشرفته |
|
||||
| `/admin/policies/{uuid}/simulate` | گزارش آزمایش + دکمهٔ فعالسازی |
|
||||
|
||||
@@ -100,6 +100,39 @@
|
||||
|
||||
---
|
||||
|
||||
## چرا آزمایش اجباری است
|
||||
|
||||
بند ۱۷ مستند، ریسک دوم: «کاربر غیرفنی نمیتواند قانون درست تعریف کند → قانونهای اشتباه،
|
||||
رفتار عجیب». موتور قانون بدون آزمایشگاه یک API قدرتمند است که هیچکس نمیتواند درست از
|
||||
آن استفاده کند.
|
||||
|
||||
پس `activate` یک شرط دارد: یک اجرای آزمایشیِ **همین نسخه** باید ثبت شده باشد. آزمایش
|
||||
قانون را روی نوبتهای واقعیِ گذشته اجرا میکند و میگوید چند نوبت تغییر میکردند و دقیقاً
|
||||
چه تغییری. عددِ «۷۵٪ نوبتها رد میشدند» چیزی است که کاربر غیرفنی هم میفهمد.
|
||||
|
||||
نسخهمحور بودن شرط عمدی است: کاربری که گزارش را دید و بعد متن قانون را عوض کرد، دیگر
|
||||
گزارشی از قانونِ فعلی ندارد.
|
||||
|
||||
### هیچ چیز ثبت نمیشود — سه لایه
|
||||
|
||||
۱. ارزیابی روی **حقایق** انجام میشود نه روی entity؛ هیچ entity ای تغییر نمیکند.
|
||||
۲. کل اجرا در تراکنشی است که در `finally` همیشه `rollback` و `clear` میشود. `clear`
|
||||
اختیاری نیست: entity های لمسشده در identity map میمانند و اولین `flush` بعدی در
|
||||
همان request ثبتشان میکند — باگی که پیدا کردنش روزها میبرد.
|
||||
۳. `PolicySimulationTest::testSimulationWritesNothingButItsOwnRun` تعداد ردیف جدولهای
|
||||
حساس را قبل و بعد میشمارد.
|
||||
|
||||
خودِ `PolicySimulationRun` **بعد** از این بلوک و در تراکنش خودش ثبت میشود.
|
||||
|
||||
### دو حالتِ مرزی که عمداً موفقاند
|
||||
|
||||
- **محیط بدون نوبت گذشته** → گزارش خالی با `warning`. اگر خطا بود، کلینیک تازه هرگز
|
||||
نمیتوانست قانونی فعال کند.
|
||||
- **قانونی که هیچ نوبتی را تغییر نمیدهد** → موفق ولی با شدت `none`، که خودش هشدار
|
||||
است: شرط احتمالاً هرگز برقرار نمیشود.
|
||||
|
||||
---
|
||||
|
||||
## تصمیمهای ثبتشده و انحرافها
|
||||
|
||||
| موضوع | تصمیم | دلیل |
|
||||
@@ -107,5 +140,6 @@
|
||||
| یک `PolicyResolver` بهجای شش موتور جدا | یک resolver + یک نقطهٔ اجرا در هر سرویس مقصد | شش کلاس با همان بدنه فقط تکرار بود؛ تفاوت واقعی در حقایق است که هر نقطه خودش میسازد |
|
||||
| `spacing` در لحظهٔ رزرو موقت، نه در تولید کاندید | رد کردن هنگام `hold` | نگه داشتن تعداد کوئریِ `AvailabilityEngine` ثابت؛ **هزینهاش** این است که اسلات نمایش داده میشود و بعد رد؛ بستنِ آن در تولید کاندید به تسک ۱۳ موکول شد |
|
||||
| `specificity` هنگام اجرا حساب میشود | متد `Policy::specificity()` | ستون ذخیرهشده باید با تغییر دامنه همزمان بهروز بماند؛ محاسبهٔ درجا سه مقایسهٔ صحیح است |
|
||||
| `appointments.applied_policies` ساخته نشد | فعلاً `PriceSnapshot.sources.applied_policies` | نوبتهای بدون فاکتور هنوز ردپای قانون ندارند — تسک ۱۰ |
|
||||
| `appointments.applied_policies` ساخته نشد | فعلاً `PriceSnapshot.sources.applied_policies` | نوبتهای بدون فاکتور هنوز ردپای قانون ندارند — تسک ۱۴ (رویدادها) |
|
||||
| یک `PolicyResolver::evaluateOne()` بهجای `evaluateIsolated()` روی شش موتور | همان resolver، بدون رقابت و ترکیب | شش موتور جدایی وجود ندارد که متد بگیرد؛ رفتار همان است |
|
||||
| عملگر `days_since` اضافه نشد | `min_days_between` مستقیم فاصله را میسنجد | تنها مصرفش همان دستهٔ `spacing` بود؛ عملگری که یک مصرف دارد، اثر است نه عملگر |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# چکلیست — تسک ۱۰ (فرم ساخت قانون و محیط آزمایش)
|
||||
|
||||
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||
**وضعیت کلی:** ✅ تمامشده با انحرافهای ثبتشده · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۹
|
||||
|
||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
|
||||
@@ -11,92 +11,95 @@
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۰.۲ | شبیهسازی **هیچ ردیفی** نمینویسد (جز `policy_simulation_runs`) | ⏳ | ⭐⭐ با شمارش ردیف اثبات شود |
|
||||
| ۰.۳ | نوبتهای واقعی بیماران در شبیهسازی تغییر نکردند | ⏳ | |
|
||||
| ۰.۱ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۰.۲ | شبیهسازی **هیچ ردیفی** نمینویسد (جز `policy_simulation_runs`) | ✅ | ⭐⭐ `testSimulationWritesNothingButItsOwnRun` پنج جدول را قبل/بعد میشمارد |
|
||||
| ۰.۳ | نوبتهای واقعی بیماران تغییر نکردند | ✅ | ارزیابی روی حقایق است، نه entity |
|
||||
|
||||
## ۱. بکاند — شبیهساز
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۱.۱ | `PolicySimulator` · `SimulationSampler` · `PolicySimulationRun` | ⏳ | |
|
||||
| ۱.۲ | سه لایهٔ تضمین: DTO · تراکنش با rollback در `finally` · تست شمارش | ⏳ | ⭐ |
|
||||
| ۱.۳ | `$this->em->clear()` بعد از rollback | ⏳ | ⭐ وگرنه entity کثیف در identity map |
|
||||
| ۱.۴ | `PolicySimulationRun` **بعد از** rollback و در تراکنش جدا ثبت میشود | ⏳ | |
|
||||
| ۱.۵ | `evaluateIsolated()` — فقط همان قانون، بدون `Resolver` و `Combiner` | ⏳ | |
|
||||
| ۱.۶ | فیلتر شعبه و سرویس از **خودِ شرط قانون** استخراج میشود | ⏳ | وگرنه «۰٪ تحت تأثیر» گمراهکننده |
|
||||
| ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ⏳ | |
|
||||
| ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ⏳ | |
|
||||
| ۱.۹ | `activate` شرط `simulate` **همان نسخه** را میسنجد | ⏳ | ⭐ نسخهٔ ۱ اجازهٔ نسخهٔ ۲ نمیدهد |
|
||||
| ۱.۱۰ | محیط بدون نوبت → `simulate` خالی موفق، `activate` مجاز | ⏳ | ⭐ کلینیک جدید قفل نشود |
|
||||
| ۱.۱۱ | چهار سطح شدت با آستانههای مستند | ⏳ | |
|
||||
| ۱.۱۲ | دو endpoint | ⏳ | |
|
||||
| ۱.۱۳ | `app:policy:prune-simulations` — آخرین اجرا per (policy, version) هرگز حذف نمیشود | ⏳ | `activate` به آن وابسته است |
|
||||
| ۱.۱ | `PolicySimulator` · `SimulationSampler` · `PolicySimulationRun` | ✅ | بهعلاوهٔ `SimulationFacts` |
|
||||
| ۱.۲ | سه لایهٔ تضمین | ✅ | ⭐ حقایق (نه entity) · تراکنش با rollback در `finally` · تست شمارش |
|
||||
| ۱.۳ | `$this->em->clear()` بعد از rollback | ✅ | ⭐ قانون بعد از `clear` دوباره خوانده میشود |
|
||||
| ۱.۴ | ثبت نتیجه **بعد از** rollback و در تراکنش جدا | ✅ | |
|
||||
| ۱.۵ | ارزیابی جدا — فقط همان قانون | ⚠️ | `PolicyResolver::evaluateOne()` بهجای `evaluateIsolated()` روی شش موتور؛ شش موتوری وجود ندارد که متد بگیرد (انحراف تسک ۰۹) |
|
||||
| ۱.۶ | فیلتر شعبه/سرویس/دسته از دامنهٔ قانون | ⚠️ | از **دامنهٔ** قانون استخراج میشود، نه از داخل `condition`؛ شرطها فیلدِ id ندارند که به کوئری تبدیل شوند |
|
||||
| ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ✅ | `testSampleSizeAboveTheCapIsRejected` |
|
||||
| ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ✅ | شش الگو |
|
||||
| ۱.۹ | `activate` شرط `simulate` **همان نسخه** | ✅ | ⭐ `testSimulationOfTheOldVersionDoesNotUnlockTheNewOne` |
|
||||
| ۱.۱۰ | محیط بدون نوبت → آزمایش خالی موفق، فعالسازی مجاز | ✅ | ⭐ `warning: دادهای برای آزمایش نیست` |
|
||||
| ۱.۱۱ | چهار سطح شدت با آستانههای مستند | ✅ | `PolicySimulationRun::severityFor()` — ۰٪ · ≤۲۰٪ · ≤۶۰٪ · >۶۰٪ |
|
||||
| ۱.۱۲ | دو endpoint | ✅ | سه تا: `policy-templates` · `simulate` · `simulations` |
|
||||
| ۱.۱۳ | `app:policy:prune-simulations` — آخرین اجرا per (قانون، نسخه) حذف نمیشود | ✅ | `--days` و `--dry-run`؛ SQL با `MAX(id) GROUP BY policy_id, policy_version` |
|
||||
|
||||
## ۲. دیتابیس
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۲.۱ | `policy_simulation_runs` با جفت tenant | ⏳ | |
|
||||
| ۲.۲ | `report` سقف ۵۰ ردیف | ⏳ | |
|
||||
| ۲.۳ | `TenantSchemaCoverageTest` سبز | ⏳ | |
|
||||
| ۲.۱ | `policy_simulation_runs` با جفت tenant | ✅ | `Version20260731065427` + دو ایندکس |
|
||||
| ۲.۲ | `report` سقف ۵۰ ردیف | ✅ | از سقف نمونه میآید: بیش از ۵۰ نوبت اصلاً خوانده نمیشود |
|
||||
| ۲.۳ | `TenantSchemaCoverageTest` سبز | ✅ | |
|
||||
|
||||
## ۳. UI
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۳.۱ | `PoliciesPage` · `PolicyFormPage` · `PolicySimulationPage` | ⏳ | |
|
||||
| ۳.۲ | `PolicyConditionBuilder` **از `GET /policy-schema`** ساخته میشود | ⏳ | ⭐ هیچ فیلد hard-code |
|
||||
| ۳.۳ | عملگرها per فیلد **فیلتر** میشوند، نه همه | ⏳ | ⭐ وگرنه ۴۲۲ بیتوضیح |
|
||||
| ۳.۴ | نوع ورودی مقدار از `schema.fields[f].type` | ⏳ | |
|
||||
| ۳.۵ | همهٔ select ها `SearchableSelect`؛ هیچ `<select>` بومی | ⏳ | |
|
||||
| ۳.۶ | انتخاب الگو → فرم کوتاه مقدارها (مسیر ۹۰٪ کاربران) | ⏳ | |
|
||||
| ۳.۷ | ستون «وضعیت فعلی → با این قانون» در گزارش | ⏳ | ⭐ تنها چیزی که کاربر غیرفنی میفهمد |
|
||||
| ۳.۸ | درصد تحت تأثیر + سطح شدت با رنگ توکنمحور | ⏳ | |
|
||||
| ۳.۹ | شدت `none` هم هشدار میدهد، با متن دوحالتی | ⏳ | |
|
||||
| ۳.۱۰ | شدت `high` → متن «مطمئنید؟» روی دکمهٔ فعالسازی | ⏳ | |
|
||||
| ۳.۱۱ | `ConfirmDialog` موجود برای فعالسازی | ⏳ | نه مودال دستساز |
|
||||
| ۳.۱۲ | `DataTable` برای لیست قوانین با فیلتر دسته/وضعیت در URL | ⏳ | |
|
||||
| ۳.۱۳ | `backTo`/`BackButton` روی هر سه صفحه | ⏳ | |
|
||||
| ۳.۱۴ | هیچ رنگ/شعاع hard-code — رنگهای شدت هم از توکن وضعیت | ⏳ | `--warning` `--danger` `--success` |
|
||||
| ۳.۱۵ | دارکمود و حالت فشرده | ⏳ | |
|
||||
| ۳.۱۶ | RTL و موبایل — جدول گزارش اسکرول افقی داخلی | ⏳ | |
|
||||
| ۳.۱۷ | تاریخها شمسی | ⏳ | |
|
||||
| ۳.۱۸ | همهٔ رشتهها فارسی | ⏳ | |
|
||||
| ۳.۱۹ | نمایش تاریخچهٔ نسخهها با diff | ⏳ | |
|
||||
| ۳.۱ | `PoliciesPage` · `PolicyFormPage` · `PolicySimulationPage` | ✅ | + ورودی «قوانین» در منوی تنظیمات |
|
||||
| ۳.۲ | `PolicyConditionBuilder` از `GET /policy-schema` | ✅ | ⭐ تست با فیلد ساختگی: بدون تغییر فرانت در UI ظاهر میشود |
|
||||
| ۳.۳ | عملگرها per فیلد فیلتر میشوند | ✅ | ⭐ `field_meta[].operators` — تست جداگانه |
|
||||
| ۳.۴ | نوع ورودی مقدار از schema | ✅ | int · enum · bool · list |
|
||||
| ۳.۵ | همهٔ selectها `SearchableSelect` | ✅ | هیچ `<select>` بومی |
|
||||
| ۳.۶ | انتخاب الگو → فرم کوتاه مقدارها | ✅ | حالت پیشفرض صفحه همین است |
|
||||
| ۳.۷ | ستون «وضعیت فعلی ← با این قانون» | ✅ | ⭐ |
|
||||
| ۳.۸ | درصد + شدت با رنگ توکنمحور | ✅ | کلاسهای `badge green/amber/red` موجود |
|
||||
| ۳.۹ | شدت `none` هشدار میدهد | ✅ | «احتمالاً شرطش هرگز برقرار نمیشود» |
|
||||
| ۳.۱۰ | شدت `high` → تأیید دوم | ✅ | |
|
||||
| ۳.۱۱ | `ConfirmDialog` موجود | ✅ | نه `window.confirm` |
|
||||
| ۳.۱۲ | `DataTable` + فیلتر دسته در URL | ✅ | `useUrlState` |
|
||||
| ۳.۱۳ | `backTo` روی هر سه صفحه | ✅ | |
|
||||
| ۳.۱۴ | هیچ رنگ/شعاع hard-code | ✅ | فقط `var(--…)` |
|
||||
| ۳.۱۵ | دارکمود و حالت فشرده | ⚠️ | فقط توکنهای موجود استفاده شده؛ بازبینی چشمی انجام نشد |
|
||||
| ۳.۱۶ | RTL و موبایل — اسکرول افقی جدول | ✅ | `overflow-x: auto` دور جدول گزارش |
|
||||
| ۳.۱۷ | تاریخها شمسی | ✅ | `formatDate` |
|
||||
| ۳.۱۸ | همهٔ رشتهها فارسی | ✅ | |
|
||||
| ۳.۱۹ | تاریخچهٔ نسخهها با diff | ⚠️ | فهرست نسخهها با اثرهای هر نسخه نمایش داده میشود؛ diff بصری واقعی نیست |
|
||||
|
||||
## ۴. تست
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۴.۱ | `PolicySimulatorTest` — شمارش ردیف قبل/بعد | ⏳ | ⭐⭐ |
|
||||
| ۴.۲ | `PolicySimulatorTest` — استثنا در `evaluateIsolated` → rollback + clear | ⏳ | |
|
||||
| ۴.۳ | `SimulationSamplerTest` — استخراج فیلتر، فقط confirmed/completed، سقف ۵۰ | ⏳ | |
|
||||
| ۴.۴ | `PolicyActivationGuardTest` — چهار حالت | ⏳ | ⭐ |
|
||||
| ۴.۵ | `PolicyTemplateTest` — هر الگو قانون معتبر تولید میکند (dataProvider) | ⏳ | ⭐ |
|
||||
| ۴.۶ | `SeverityTest` — چهار آستانه | ⏳ | |
|
||||
| ۴.۷ | `PolicyFormPage.test.tsx` — فیلد ساختگی از mock schema در UI ظاهر میشود | ⏳ | ⭐ |
|
||||
| ۴.۸ | `PolicyFormPage.test.tsx` — عملگر نامعتبر برای نوع نمایش داده نمیشود | ⏳ | |
|
||||
| ۴.۱ | شمارش ردیف قبل/بعد | ✅ | ⭐⭐ |
|
||||
| ۴.۲ | استثنا → rollback + clear | ⚠️ | `finally` تضمینش میکند ولی تست تزریق استثنا نوشته نشد |
|
||||
| ۴.۳ | نمونهگیری — فقط confirmed/completed، سقف | ✅ | سقف تست شد؛ فیلتر وضعیت غیرمستقیم (نوبتهای نمونه completed اند) |
|
||||
| ۴.۴ | دروازهٔ فعالسازی | ✅ | ⭐ بدون آزمایش، نسخهٔ قدیمی، نسخهٔ درست، محیط خالی |
|
||||
| ۴.۵ | هر الگو قانون معتبر میسازد | ⚠️ | یک الگو کامل تست شد (`vip_discount`) + ورودی ناقص؛ dataProvider ششتایی نوشته نشد |
|
||||
| ۴.۶ | چهار آستانهٔ شدت | ⚠️ | `high` و `none` تست شدند؛ `low`/`medium` نه |
|
||||
| ۴.۷ | فیلد ساختگی از mock schema در UI | ✅ | ⭐ `PolicyFormPage.test.tsx` |
|
||||
| ۴.۸ | عملگر نامعتبر نمایش داده نمیشود | ✅ | |
|
||||
| ۴.۹ | صفحهٔ آزمایش — قفل فعالسازی روی نسخهٔ قدیمی | ✅ | `PolicySimulationPage.test.tsx` |
|
||||
|
||||
**اجرا:** `ddev exec php bin/phpunit tests/Policy` → ۳۰ تست · `npx vitest run` → ۶۲۸ تست.
|
||||
|
||||
## ۵. مستندات
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۵.۱ | `docs/api/policy.md` — `simulate`، `policy-templates`، شرط جدید `activate` | ⏳ | |
|
||||
| ۵.۲ | `docs/architecture/policy-engine.md` بخش «چرا آزمایش اجباری است» | ⏳ | ارجاع به ریسک دوم مستند |
|
||||
| ۵.۱ | `docs/api/policy.md` — `simulate`، `policy-templates`، شرط تازهٔ `activate` | ✅ | JSON واقعی |
|
||||
| ۵.۲ | `policy-engine.md` بخش «چرا آزمایش اجباری است» | ✅ | + سه لایهٔ تضمین و دو حالت مرزی |
|
||||
|
||||
## ۶. بازبینی پایانی
|
||||
|
||||
| # | مورد | وضعیت | یادداشت |
|
||||
|---|---|---|---|
|
||||
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ⏳ | |
|
||||
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
|
||||
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
|
||||
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
|
||||
| ۶.۶ | تستهای tenant سبز | ⏳ | |
|
||||
| ۶.۷ | `docs/api/*` بهروز | ⏳ | |
|
||||
| ۶.۸ | چکلیست UI کامل | ⏳ | |
|
||||
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | این تسک قرارداد عمومی عوض نمیکند |
|
||||
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
|
||||
| ۶.۱۱ | موارد بهتعویق با دلیل و تسک مقصد | ⏳ | |
|
||||
| ۶.۱ | هیچ 🔄 و ⏳ بیدلیل نمانده | ✅ | ۷ مورد ⚠️ همه با دلیل |
|
||||
| ۶.۲ | `bin/phpunit` کامل سبز | ✅ | ۱۲۵۰ تست |
|
||||
| ۶.۳ | `--group=slot-mode-frozen` سبز | ✅ | |
|
||||
| ۶.۴ | `phpstan` بدون خطای جدید | ✅ | ۱۴ = baseline |
|
||||
| ۶.۵ | `npx tsc --noEmit` و تستهای فرانت سبز | ✅ | ۶۲۸ تست؛ **`yarn test` داخل ddev اجرا نمیشود** (باینری esbuild برای darwin نصب شده) — روی هاست اجرا شد |
|
||||
| ۶.۶ | تستهای tenant سبز | ✅ | |
|
||||
| ۶.۷ | `docs/api/*` بهروز | ✅ | |
|
||||
| ۶.۸ | چکلیست UI کامل | ✅ | جز ۳.۱۵ و ۳.۱۹ |
|
||||
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ✅ | این تسک هیچ قرارداد عمومیای عوض نکرد؛ همهٔ اندپوینتها پنلمحورند |
|
||||
| ۶.۱۰ | commit، سپس `graphify update .` | ✅ | دو کامیت جدا |
|
||||
| ۶.۱۱ | موارد بهتعویق با دلیل | ✅ | ۳.۱۵ (بازبینی چشمی) · ۳.۱۹ (diff بصری) · ۴.۲/۴.۵/۴.۶ (پوشش تست) — همه در همین فایل ثبتاند |
|
||||
|
||||
Reference in New Issue
Block a user