- 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.
159 lines
7.6 KiB
Markdown
159 lines
7.6 KiB
Markdown
# نکات پیادهسازی — تسک ۱۴
|
||
|
||
## ۱. outbox، نه dispatch مستقیم
|
||
|
||
تسکهای ۰۷ تا ۱۳ هر کدام یک `bus->dispatch()` دارند. این تسک همه را به
|
||
`publisher->record()` تغییر میدهد:
|
||
|
||
```php
|
||
// قبل
|
||
$this->bus->dispatch((new Envelope($event))->with(new DispatchAfterCurrentBusStamp()));
|
||
|
||
// بعد
|
||
$this->publisher->record($event); // فقط persist — داخل همان تراکنش کاری
|
||
```
|
||
|
||
`DispatchAfterCurrentBusStamp` مشکل «rollback بعد از پیامک» را حل میکند ولی مشکل
|
||
«commit موفق، Redis پایین» را نه. outbox هر دو را حل میکند.
|
||
|
||
اگر تسکهای قبلی هنوز اجرا نشدهاند، از روز اول `record()` بنویس.
|
||
|
||
## ۲. payload فقط uuid
|
||
|
||
```php
|
||
// ❌ entity در پیام async
|
||
new AppointmentBooked($appointment);
|
||
|
||
// ✅
|
||
new AppointmentBooked(['appointment_uuid' => $appointment->getUuid()]);
|
||
```
|
||
|
||
entity در پیام یعنی: سریالسازی سنگین، detach شدن از EntityManager، و دادهای که تا لحظهٔ
|
||
مصرف کهنه شده. مصرفکننده با uuid خودش واکشی میکند و تازهترین حالت را میبیند.
|
||
|
||
## ۳. idempotency در مصرفکننده، نه در انتشار
|
||
|
||
messenger ممکن است یک پیام را دوبار تحویل دهد (at-least-once). پس **مصرفکننده** باید
|
||
idempotent باشد:
|
||
|
||
```php
|
||
public function __invoke(AppointmentBooked $event): void
|
||
{
|
||
if ($this->smsLogRepo->alreadySent($event->uuid, 'booking_confirmation')) {
|
||
return;
|
||
}
|
||
…
|
||
}
|
||
```
|
||
|
||
`domain_events.uuid` همان کلید idempotency است. تلاش برای تضمین exactly-once در سمت
|
||
انتشار، مسئلهای است که حل نمیشود؛ idempotent بودن مصرفکننده حل میشود.
|
||
|
||
## ۴. `active_ratio` — عدد اصلی این تسک
|
||
|
||
```
|
||
utilization = occupied / available
|
||
active_ratio = active / occupied
|
||
```
|
||
|
||
`utilization` عدد فروش است و کلینیک دوستش دارد. `active_ratio` عدد **تشخیص** است:
|
||
|
||
| `active_ratio` | معنی |
|
||
|---|---|
|
||
| > ۰.۸ | تعریف بخشها درست است |
|
||
| ۰.۵ – ۰.۸ | زمان passive/انتظار قابل توجه — بازبینی |
|
||
| < ۰.۳ | **تعریف اشتباه** — منبع به بخشی نسبت داده شده که در آن کار نمیکند |
|
||
|
||
مثال واقعی: اپراتوری که اشتباهاً به بخش «انتظار اثر بیحسی» هم نسبت داده شده،
|
||
`active_ratio` حدود ۰.۵ میگیرد — و همان لحظهای است که کلینیک میفهمد ۳۰ دقیقه ظرفیت
|
||
هر نوبت را الکی میسوزاند.
|
||
|
||
این توضیح باید **در خود UI** باشد (tooltip روی ستون)، نه فقط در مستندات.
|
||
|
||
## ۵. `available_minutes = 0` → `utilization = null`
|
||
|
||
```php
|
||
'utilization' => $available > 0 ? round($occupied / $available, 2) : null,
|
||
```
|
||
|
||
نه صفر. منبعی که تقویم ندارد، «بهرهوری صفر» ندارد — بهرهوریاش **تعریفنشده** است.
|
||
صفر نشان دادن یعنی کلینیک فکر میکند منبع بیاستفاده است در حالی که مشکل نبود تقویم است.
|
||
|
||
در UI: `—` با tooltip «تقویم کاری تعریف نشده» + لینک به تنظیم تقویم (تسک ۰۳).
|
||
|
||
## ۶. مرز بازه در کوئری
|
||
|
||
```sql
|
||
AND ro.start_at >= :from AND ro.start_at < :to
|
||
```
|
||
|
||
نه `end_at <= :to`. نوبتی که ۲۳:۳۰ شروع شده و ۰۰:۳۰ روز بعد تمام میشود، باید در روز
|
||
شروعش شمرده شود. با شرط `end_at` کامل حذف میشود.
|
||
|
||
## ۷. `plan-accuracy` — اول منبع داده را بررسی کن
|
||
|
||
کوئری این گزارش به `patient_sessions.appointment_id` و `started_at`/`ended_at` نیاز دارد.
|
||
**پیش از پیادهسازی** بررسی کن که این ستونها هستند:
|
||
|
||
```bash
|
||
ddev exec php bin/console doctrine:mapping:describe 'App\Patient\Entity\PatientSession'
|
||
```
|
||
|
||
اگر نیستند، جایگزین: `appointment_events` — فاصلهٔ بین انتقال به `salon` و به `completed`.
|
||
اگر آن هم قابل اتکا نیست، **این گزارش را به تسک جدا موکول کن** و در README تسکها بنویس.
|
||
گزارشی با داده حدسی بدتر از نبود گزارش است: کلینیک بر اساسش بخشها را عوض میکند.
|
||
|
||
## ۸. edge case ها
|
||
|
||
| حالت | رفتار درست |
|
||
|---|---|
|
||
| منبع بدون تقویم | `utilization: null` + لینک تنظیم تقویم |
|
||
| منبع بدون هیچ اشغال | `occupied: 0, active_ratio: null` |
|
||
| بخش `passive` | در `occupied` هست، در `active` نه |
|
||
| `setup/cleanup` | در `occupied` هست (منبع واقعاً اشغال بود) |
|
||
| اشغال `released` (لغوشده) | در گزارش **نمیآید** — `status='booked'` فقط |
|
||
| اشغال دستی (`appointment_id IS NULL`) | در `occupied` میآید، `appointment_count` تحت تأثیر نیست |
|
||
| منبع با `capacity=3` | `occupied` جمع همهٔ واحدهاست؛ `available` باید × capacity شود |
|
||
| نمونهٔ کمتر از ۱۰ در `plan-accuracy` | `severity: 'insufficient_data'`، عدد نمایش داده نشود |
|
||
| انحراف منفی بزرگ (پیشبینی > واقعیت) | `severity: 'high'` — همانقدر مهم |
|
||
| بازه > ۹۰ روز | `422` |
|
||
| رویداد شکستخورده با ۵ تلاش | ردیف میماند، در `GET /domain-events` با نشان خطا |
|
||
|
||
سطر `capacity=3` را فراموش نکن: اتاق سهتخته در ۸ ساعت، ۲۴ نفر-ساعت ظرفیت دارد نه ۸.
|
||
بدون ضرب در `capacity`، بهرهوریاش سه برابر واقعی نشان داده میشود.
|
||
|
||
## ۹. تست
|
||
|
||
```
|
||
tests/Shared/Event/OutboxTest.php ← ⭐
|
||
- record() داخل تراکنش → ردیف در همان تراکنش
|
||
- rollback → هیچ ردیفی و هیچ انتشاری
|
||
- worker ردیف را منتشر و published_at را پر میکند
|
||
- شکست → attempts++ و last_error
|
||
- attempts >= 5 → دیگر برداشته نمیشود، حذف هم نمیشود
|
||
tests/Shared/Event/EventPayloadTest.php
|
||
- payload فقط اسکالر و uuid (reflection روی همهٔ زیرکلاسهای DomainEvent)
|
||
- هر رویداد entity_type/entity_id دارد
|
||
tests/Report/ResourceUtilizationTest.php ← ⭐
|
||
- passive در occupied هست، در active نه
|
||
- setup/cleanup در occupied
|
||
- released شمرده نمیشود
|
||
- capacity=3 → available × 3
|
||
- منبع بدون تقویم → utilization null (نه صفر)
|
||
- مرز بازه: نوبت شبگذر در روز شروعش
|
||
tests/Report/PlanAccuracyTest.php
|
||
- انحراف مثبت و منفی هر دو high
|
||
- نمونهٔ < ۱۰ → insufficient_data
|
||
tests/Report/ReportAuthTest.php
|
||
- منشی روی domain-events → 403
|
||
- بازه > ۹۰ روز → 422
|
||
tests/Report/ReportQueryCountTest.php
|
||
- گزارش ۹۰ روزه: تعداد کوئری ثابت، مستقل از تعداد منبع
|
||
```
|
||
|
||
## ۱۰. مستندات
|
||
|
||
- `docs/api/reports.md` — دو گزارش + معنی هر عدد + جدول `active_ratio`
|
||
- `docs/architecture/domain-events.md` — قرارداد رویداد، فهرست کامل، الگوی outbox،
|
||
تفاوت با `AppointmentEvent`، و قاعدهٔ idempotency مصرفکننده
|