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,151 @@
|
||||
# معماری — تسک ۱۴
|
||||
|
||||
## ساختار فایل
|
||||
|
||||
```
|
||||
src/Shared/Event/
|
||||
├── DomainEvent.php # کلاس پایه — payload فقط اسکالر و uuid
|
||||
├── DomainEventPublisher.php # تنها نقطهٔ انتشار
|
||||
├── Entity/DomainEventLog.php # outbox
|
||||
└── MessageHandler/PublishDomainEventHandler.php
|
||||
|
||||
src/Report/
|
||||
├── Service/
|
||||
│ ├── ResourceUtilizationReporter.php
|
||||
│ └── PlanAccuracyReporter.php
|
||||
├── Dto/{UtilizationRow, AccuracyRow}.php
|
||||
└── Controller/ReportController.php
|
||||
```
|
||||
|
||||
## قرارداد رویداد
|
||||
|
||||
```php
|
||||
abstract class DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $entityType, // محیط — همهٔ رویدادها tenant دارند
|
||||
public readonly int $entityId,
|
||||
public readonly array $payload, // فقط اسکالر و uuid
|
||||
public readonly int $occurredAt,
|
||||
) {}
|
||||
|
||||
abstract public function name(): string; // 'AppointmentBooked'
|
||||
}
|
||||
```
|
||||
|
||||
سه قاعدهٔ غیرقابلمذاکره:
|
||||
|
||||
1. **payload فقط uuid و اسکالر** — هیچ entity ای در رویداد نیست. مصرفکننده خودش
|
||||
واکشی میکند. entity در پیام async یعنی سریالسازی، detach شدن، و دادهی کهنه.
|
||||
2. **انتشار بعد از commit** — با `DispatchAfterCurrentBusStamp` یا از راه outbox.
|
||||
3. **هر رویداد محیط دارد** — مصرفکننده باید بداند رویداد مال کدام محیط است، وگرنه
|
||||
پیامک کلینیک الف به شمارهٔ کلینیک ب میرود.
|
||||
|
||||
## outbox — چرا لازم است
|
||||
|
||||
```
|
||||
تراکنش: [ثبت نوبت] + [درج ردیف در domain_events] → commit اتمی
|
||||
بعد: PublishDomainEventHandler ردیف را برمیدارد و به messenger میدهد
|
||||
```
|
||||
|
||||
بدون outbox دو حالت شکست ممکن است:
|
||||
|
||||
| حالت | نتیجه |
|
||||
|---|---|
|
||||
| dispatch قبل از commit، تراکنش rollback | پیامک رفته، نوبتی وجود ندارد |
|
||||
| commit موفق، dispatch شکست خورد (Redis down) | نوبت هست، هیچکس مطلع نشد |
|
||||
|
||||
با outbox، ردیف رویداد **در همان تراکنش** ثبت میشود. یک worker (یا `scheduler` هر ۱۰
|
||||
ثانیه) ردیفهای `published_at IS NULL` را برمیدارد و منتشر میکند. حداکثر یک بار
|
||||
تأخیر، هرگز گمشدن.
|
||||
|
||||
```php
|
||||
final class DomainEventPublisher
|
||||
{
|
||||
/** داخل تراکنش کاری صدا زده میشود — فقط درج، بدون I/O خارجی. */
|
||||
public function record(DomainEvent $event): void
|
||||
{
|
||||
$this->em->persist(DomainEventLog::from($event));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
تسکهای ۰۷ تا ۱۳ بهجای `bus->dispatch()` باید `publisher->record()` صدا بزنند.
|
||||
اگر آن تسکها تمام شدهاند، این تسک شامل جایگزینی آن فراخوانیها هم است.
|
||||
|
||||
## `ResourceUtilizationReporter`
|
||||
|
||||
```php
|
||||
/** @return UtilizationRow[] */
|
||||
public function report(EntityContext $ctx, int $from, int $to, ?Branch $branch): array
|
||||
```
|
||||
|
||||
چهار عدد per منبع:
|
||||
|
||||
| عدد | از کجا | معنی |
|
||||
|---|---|---|
|
||||
| `available_minutes` | `ResourceAvailabilityService::rawWindows()` (تسک ۰۳) | ظرفیت تقویمی |
|
||||
| `occupied_minutes` | `SUM(end_at - start_at)` روی `resource_occupancy` با `status='booked'` | زمان اشغال، شامل setup/cleanup و passive |
|
||||
| `active_minutes` | همان، ولی `occupancy_kind != 'passive'` | زمان کار واقعی |
|
||||
| `wasted_minutes` | `occupied - active` | زمانی که منبع رزرو بود ولی کار نمیکرد |
|
||||
|
||||
```
|
||||
utilization = occupied / available → «چقدر از ظرفیت فروخته شد»
|
||||
active_ratio = active / occupied → «چقدر از اشغال، کار واقعی بود»
|
||||
```
|
||||
|
||||
`active_ratio` پایین دقیقاً همان چیزی است که مستند بند ۱۷ میخواهد کشف کند: منبعی که
|
||||
۷۰٪ زمانش «رزرو ولی بیکار» است، یعنی بخشهای نوبت اشتباه تعریف شدهاند — مثلاً اپراتور
|
||||
به بخش «انتظار» نسبت داده شده که نباید.
|
||||
|
||||
## `PlanAccuracyReporter`
|
||||
|
||||
مقایسهٔ پیشبینی و واقعیت per سرویس:
|
||||
|
||||
```php
|
||||
// پیشبینی: appointments.plan_total_minutes (تسک ۰۷)
|
||||
// واقعیت: patient_sessions یا appointment_events (زمان بین ورود و پایان)
|
||||
$deviation = intdiv(($actualAvg - $plannedAvg) * 100, max(1, $plannedAvg));
|
||||
```
|
||||
|
||||
| انحراف | شدت | معنی |
|
||||
|---|---|---|
|
||||
| ±۱۰٪ | `ok` | تعریف درست است |
|
||||
| ±۱۰..۳۰٪ | `medium` | بازبینی بخشها |
|
||||
| > ۳۰٪ | `high` | تعریف اشتباه — ظرفیت غلط محاسبه میشود |
|
||||
|
||||
انحراف **منفی** بزرگ هم مشکل است: سرویسی که ۹۰ دقیقه پیشبینی شده و ۴۵ دقیقه طول
|
||||
میکشد، نصف ظرفیت کلینیک را الکی میبلعد — همان مسئلهای که کل این پروژه برای حلش است.
|
||||
|
||||
حداقل نمونه: ۱۰ مراجعهٔ `completed`. کمتر از آن، `severity: 'insufficient_data'`.
|
||||
|
||||
## کارایی گزارشها
|
||||
|
||||
هر دو گزارش کوئری تجمعیاند، نه پیمایش:
|
||||
|
||||
```sql
|
||||
SELECT ro.resource_id,
|
||||
SUM(ro.end_at - ro.start_at) AS occupied,
|
||||
SUM(CASE WHEN ro.occupancy_kind <> 'passive' THEN ro.end_at - ro.start_at ELSE 0 END) AS active
|
||||
FROM resource_occupancy ro
|
||||
WHERE ro.entity_type = :type AND ro.entity_id = :id
|
||||
AND ro.status = 'booked'
|
||||
AND ro.start_at >= :from AND ro.end_at <= :to
|
||||
GROUP BY ro.resource_id
|
||||
```
|
||||
|
||||
`idx_occ_tenant_range` تسک ۰۷ همین را پوشش میدهد. `available_minutes` جدا محاسبه
|
||||
میشود (از تقویم، کششده). سقف بازه ۹۰ روز.
|
||||
|
||||
## پنل ادمین
|
||||
|
||||
- `ResourceUtilizationPage.tsx` — جدول منابع + نمودار میلهای با `Recharts` (در استک هست).
|
||||
ستونها: منبع، ظرفیت، اشغال، کار فعال، بهرهوری، نسبت فعال. ردیفهای
|
||||
`active_ratio < 0.3` با نشان هشدار.
|
||||
- `PlanAccuracyPage.tsx` — جدول سرویسها با انحراف و شدت + لینک به
|
||||
«ویرایش بخشهای این سرویس» (تسک ۰۵)
|
||||
|
||||
لینک به ویرایش بخشها مهمترین بخش این صفحه است: گزارشی که مشکل را نشان میدهد ولی راه
|
||||
اصلاح را نمیدهد، خوانده نمیشود.
|
||||
|
||||
بازهٔ زمانی با `PersianDatePicker`، وضعیت در URL با `useUrlState`.
|
||||
Reference in New Issue
Block a user