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,123 @@
# جریان کاربری — تسک ۰۵
## الف) کلینیک الگوی بخش‌های یک سرویس را تعریف می‌کند
```
پنل › خدمات › لیزر کندلا › تب «بخش‌های نوبت»
├─ «افزودن بخش»
│ نام: مالیدن کرم بی‌حسی
│ نوع (کلید ادغام): آماده‌سازی
│ مدت: ثابت — ۵ دقیقه
│ بیمار حاضر است: بله
│ با بخش‌های هم‌نوع ادغام شود: بله
│ └─ نیازمندی‌ها:
│ [اتاق] × ۱ — انحصاری
│ [اپراتور] × ۱ — انحصاری — مهارت: لیزر آلکساندرایت — قید: هم‌جنس با بیمار
├─ بخش ۲: انتظار اثر بی‌حسی — ثابت ۳۰ — ادغام‌پذیر
│ نیازمندی: فقط [اتاق] × ۱ انحصاری ← اپراتور اینجا آزاد است
├─ بخش ۳: خود لیزر — سهمی ۱۰۰٪
│ نیازمندی: [اتاق] × ۱ · [اپراتور] × ۱ · [دستگاه] × ۱ از استخر «لیزرهای آلکساندرایت»
└─ بخش ۴: مراقبت بعد — ثابت ۵
نیازمندی: [اتاق] × ۱ · [اپراتور] × ۱
نوار پیش‌نمایش زمانی (زیر فرم، زنده):
┌─────┬───────────────────┬─────────────┬─────┐
۵' │ ۳۰' │ ۲۰' │ ۵' │
│🏠👤 │ 🏠 │ 🏠 👤 🔧 │🏠👤 │
└─────┴───────────────────┴─────────────┴─────┘
کل: ۶۰ دقیقه · اپراتور واقعاً درگیر: ۳۰ دقیقه
«ذخیره» → PUT /api/v1/service-item/{uuid}/segments
```
خط «اپراتور واقعاً درگیر: ۳۰ دقیقه» مهم‌ترین بازخورد این صفحه است: کلینیک آنجا می‌فهمد
چرا این کار ارزشش را دارد.
---
## ب) بیمار سرویس و آیتم انتخاب می‌کند (سایت عمومی)
```
انتخاب پزشک/کلینیک
GET /api/v1/appointment-booking-services/{doctorUuid}
→ booking_mode = "resource" ← حالت جدید
→ services[] با گروه‌های آیتم
بیمار انتخاب می‌کند: ناحیه = صورت + بیکینی · سطح انرژی = ۱۶
POST /api/v1/service-selection/validate (تسک ۰۴، debounce)
→ valid: true · total_duration_minutes: 23 · total_price_rials: …
│ اگر valid=false:
│ خطاها زیر همان گروه نمایش داده می‌شوند
│ «انتخاب حداقل یک مورد از نواحی الزامی است»
│ «صورت و فول‌بادی با هم قابل انتخاب نیستند»
│ و دکمهٔ «ادامه» غیرفعال می‌ماند
POST /api/v1/appointment-plan/preview
→ total_minutes: 68
segments: [آماده‌سازی ۵ · انتظار ۳۰ · لیزر ۲۳ · مراقبت ۵ · تمیزکاری ۵]
│ اگر NoEligibleResourceException:
│ «هیچ اپراتور خانمی با مهارت لیزر آلکساندرایت در شعبهٔ مرکزی موجود نیست»
│ + پیشنهاد شعبهٔ دیگر (اگر داشته باشد)
مرحلهٔ انتخاب زمان → تسک ۰۶
```
**نکته UX:** برنامهٔ نوبت به بیمار **نمایش داده نمی‌شود**. بیمار فقط «۶۸ دقیقه» و
«توضیحات آماده‌سازی» را می‌بیند. بخش‌ها جزئیات عملیاتی کلینیک‌اند؛ نشان دادنشان به بیمار
فقط سؤال می‌سازد.
استثنا: بخش‌هایی با `patient_present = false` نباید در مدت اعلامی به بیمار بیایند
(«تمیزکاری یونیت» ۵ دقیقهٔ بعد از رفتن بیمار است). پس دو عدد وجود دارد:
- `total_minutes` = ۶۸ (اشغال کلینیک) — برای موتور
- `patient_facing_minutes` = ۶۳ — برای نمایش
هر دو در پاسخ `preview` برگردند.
---
## ج) منشی از پنل نوبت می‌سازد
همان جریان ب، با دو تفاوت:
1. `forManagement = true` — بازهٔ رزرو و خاموش بودن نوبت‌دهی آنلاین اعمال نمی‌شود
(همان رفتاری که `SlotCalculatorService::isWithinBookingWindow()` امروز دارد)
2. منشی می‌تواند **منبع را دستی انتخاب کند**: پاسخ `preview` برای هر نیازمندی
`candidates` را با نام برمی‌گرداند و پنل یک `SearchableSelect` اختیاری نشان می‌دهد.
خالی گذاشتن یعنی «تو انتخاب کن» (استراتژی تسک ۰۶).
---
## د) حالت خطا — هیچ منبعی موجود نیست
```
POST /appointment-plan/preview
422 {
"success": false,
"errors": [{
"code": "ERR_NO_ELIGIBLE_RESOURCE",
"message": "هیچ اپراتور خانمی با مهارت لیزر آلکساندرایت در شعبهٔ مرکزی موجود نیست",
"meta": { "segment": "خود لیزر", "role": "اپراتور", "branch_uuid": "…" }
}]
}
```
`meta` اجباری است: پنل با آن می‌تواند مستقیم به صفحهٔ منابع همان شعبه لینک بدهد
(«افزودن اپراتور») و کلینیک در سه کلیک مشکل را حل کند، به‌جای اینکه با یک پیام
بن‌بست بماند.