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:
hamed
2026-07-30 11:43:58 +03:30
parent 1d338503c8
commit 021d0eb6b2
62 changed files with 8098 additions and 0 deletions
@@ -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` یک بخش «چرا آزمایش اجباری است»
اضافه کن با ارجاع به ریسک دوم مستند بند ۱۷.