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,150 @@
# جریان کاربری — تسک ۰۷
## الف) مسیر موفق — بیمار از سایت عمومی
```
[تسک ۰۶] بیمار ساعت ۰۹:۰۰ را انتخاب می‌کند
POST /api/v1/appointment-hold
{ "doctor_uuid":"…", "branch_uuid":"…", "service_item_uuid":"…",
"option_uuids":["…"], "start": 1754…, "assignment": { "operator":"…", "room":"…", "device":"…" } }
├─ سرور: برنامه را دوباره می‌سازد (به assignment اعتماد نمی‌کند)
├─ نوبت pending با expires_at = now + 900
├─ appointment_segments × ۵
└─ resource_occupancy × (بخش × منبع) — بخش انتظار فقط اتاق
201 { "hold_uuid":"…", "expires_at": 1754…, "total_price_rials": … }
│ ⏱ تایمر ۱۵ دقیقه‌ای در UI: «۱۴:۵۹ برای تکمیل رزرو»
پرداخت بیعانه (اگر deposit_required) → درگاه → بازگشت
POST /api/v1/appointment-confirm { "hold_uuid":"…" }
├─ ۱ hold معتبر است؟
├─ ۲ قوانین صلاحیت و فاصله (تسک ۰۹)
├─ ۳ وضعیت → confirmed
├─ ۴ اشغال‌ها hold → booked
├─ ۵ snapshot قیمت (تسک ۰۸)
└─ ۶ رویداد AppointmentBooked (بعد از commit)
200 { "appointment_uuid":"…", "status":"confirmed" }
پیامک تأییدیه (از راه رویداد، async)
```
---
## ب) رقابت روی ساعت پرتقاضا
```
بیمار الف بیمار ب
│ ۰۹:۰۰ را می‌بیند │ ۰۹:۰۰ را می‌بیند
│ │
▼ POST /appointment-hold ▼ POST /appointment-hold
│ │
│ INSERT slot (res=7,b=…,u=0) ✓ │ INSERT slot (res=7,b=…,u=0) ✗ duplicate
▼ ▼
201 hold_uuid 409 ERR_SLOT_TAKEN
UI: «این ساعت همین لحظه رزرو شد.»
+ لیست به‌روزشدهٔ وقت‌های نزدیک
(خودکار، بدون کلیک دوباره)
```
پیام «این ساعت همین لحظه رزرو شد» + پیشنهاد جایگزین، همان کاری است که مستند بند ۱۷
برای ریسک «رقابت روی ساعت‌های پرتقاضا» می‌خواهد. `409` خالی بدون جایگزین یعنی بیمار می‌رود.
---
## ج) hold منقضی می‌شود
```
hold ساخته شد ─── ۱۵ دقیقه ───▶ منقضی
┌─────────────────────────┴──────────────────────────┐
│ │
cron هر دقیقه یا: بیمار confirm می‌زند
ExpireAppointmentsHandler │
│ ▼
├─ status → expired 409 ERR_HOLD_EXPIRED
├─ occupancy → released │
└─ occupancy_slot → DELETE ▼
▼ UI: «زمان رزرو شما به پایان رسید»
زمان دوباره در جستجو ظاهر می‌شود + بازگشت به لیست وقت‌ها
```
نکته: حتی پیش از اجرای cron، `hasRoom` تسک ۰۶ شرط `expires_at > now` را دارد، پس
hold مردهٔ چند ثانیه‌ای هم مانع کسی نمی‌شود. cron فقط تمیزکاری است.
---
## د) منشی نوبت را جابه‌جا می‌کند
```
پنل › نوبت‌ها › جزئیات نوبت › «جابه‌جایی»
انتخاب تاریخ/ساعت جدید (همان UI تسک ۰۶، با forManagement=true)
POST /api/v1/appointment/{uuid}/reschedule { "start": … , "assignment": {…} }
├─ ۱ hold جدید ساخته می‌شود ← اگر شکست: rollback کامل، نوبت قدیم سالم
├─ ۲ اشغال‌های قدیم released
├─ ۳ نوبت قدیم → rescheduled
└─ ۴ لینک قدیم ↔ جدید در appointment_events
200 { "new_appointment_uuid": "…" }
پیامک اطلاع‌رسانی جابه‌جایی
```
اگر hold جدید `409` بدهد:
```
422 { "errors": [{ "code":"ERR_SLOT_TAKEN",
"message":"زمان جدید در دسترس نیست. نوبت فعلی تغییری نکرد." }] }
```
جملهٔ دوم پیام اجباری است — منشی باید بداند وضعیت فعلی امن است و لازم نیست چیزی را
درست کند.
---
## ه) لغو نوبت
```
لغو (بیمار یا پزشک یا منشی)
├─ status → cancelled_by_user / cancelled_by_doctor
├─ active_slot_key → NULL (مکانیزم موجود، دست‌نخورده)
├─ resource_occupancy → released
└─ resource_occupancy_slot → DELETE
ظرفیت فوری آزاد می‌شود
[تسک ۱۳] لیست انتظار همان بازه اطلاع می‌گیرد
```
---
## و) مسدودسازی دستی یک منبع
حالت خاصی که `appointment_id = NULL` را توضیح می‌دهد:
```
پنل › منابع › دستگاه کندلا ۲ › «مسدودسازی بازه»
تاریخ/ساعت + دلیل: «سرویس دوره‌ای»
یک ردیف resource_occupancy با appointment_id=NULL و status='booked'
(یا معادلاً یک resource_exception از تسک ۰۳ — هر دو کار می‌کنند)
```
**تصمیم:** مسدودسازی **بلندمدت و تکرارشونده**`resource_exception` (تسک ۰۳).
مسدودسازی **موردی و کوتاه**`resource_occupancy` با `appointment_id=NULL`.
دلیل: دومی در همان ایندکس داغ می‌نشیند و در `OccupancyIndex` بدون کد اضافه دیده می‌شود.
این تفکیک را در `docs/api/appointment-booking.md` بنویس، وگرنه دو راه انجام یک کار
گیج‌کننده می‌شود.