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,235 @@
# گزارش وضعیت فعلی سیستم در برابر مستند «موتور نوبت‌دهی Clinic Pro»
مرجع: [clinic-pro-mostanad-sade.md](../clinic-pro-mostanad-sade.md)
دامنه بررسی: `clinicpro/src/**` (Symfony 7.4) + `clinicpro/assets/admin/**` (React SPA)
---
## ۱. خلاصه اجرایی
سیستم فعلی یک موتور نوبت‌دهی **تک‌منبعی (فقط پزشک)** است که اخیراً یک حالت
«نوبت‌دهی سرویسی» هم گرفته: `WeeklySchedule.meta.booking_mode = service` باعث می‌شود
طول نوبت از `ServiceItem.duration_minutes` گرفته شود به‌جای اسلات ثابت.
این با مستند در جهت درست است، ولی فقط **یک لایه از هفت لایه** مستند را پوشش می‌دهد.
سه ستون اصلی مستند اصلاً وجود ندارند:
| ستون مستند | وضعیت |
|---|---|
| تقویم مال منبع است نه پزشک (بند ۲-۱، ۶) | ❌ وجود ندارد — تقویم فقط `(doctor, clinic)` است |
| نوبت از چند بخش تشکیل شده (بند ۷) | ❌ وجود ندارد — نوبت یک `slot_start/slot_end` پیوسته است |
| موتور قوانین شش‌دسته‌ای (بند ۸) | ⚠️ فقط یک دسته (قیمت) به شکل `DiscountRule` |
نکته مثبت و مهم: **قانون سوم مستند («جلوگیری از رزرو تکراری کار دیتابیس است»)
از قبل رعایت شده** — `Appointment.active_slot_key` یک ستون `UNIQUE` است که فقط در
وضعیت‌های اشغال‌کننده مقدار می‌گیرد. همان الگو باید به `resource_occupancy` تعمیم پیدا کند.
---
## ۲. آنچه امروز داریم (کد واقعی)
### ۲-۱ محیط (tenant)
`(entity_type, entity_id)` روی ۲۰ جدول، با `entity_type ∈ {doctor, clinic}`
— [src/Shared/Tenant/TenantOwnedTrait.php](../../../src/Shared/Tenant/TenantOwnedTrait.php
سند کامل: [docs/architecture/tenancy.md](../../architecture/tenancy.md).
| سطح مستند | معادل امروز |
|---|---|
| کلینیک (Tenant) | ✅ `clinic` یا `doctor` (مطب شخصی) |
| شعبه (Branch) | ⚠️ نیم‌بند — `DoctorAddress` نقش «محل» را بازی می‌کند و در `location_id` هر شیفت می‌نشیند |
| اتاق (Room) | ❌ وجود ندارد |
`Clinic` هیچ فیلد شعبه‌ای ندارد ([src/Clinic/Entity/Clinic.php](../../../src/Clinic/Entity/Clinic.php)).
ساعت کاری شعبه هم وجود ندارد؛ ساعت کاری فقط روی برنامهٔ پزشک است.
### ۲-۲ تعریف خدمات
`ServiceSection` (بخش) → `ServiceItem` (سرویس)، هر دو tenant-دار.
[src/ClinicService/Entity/ServiceItem.php](../../../src/ClinicService/Entity/ServiceItem.php):
```php
private int $priceRials = 0;
private ?int $durationMinutes = null; // مدت، تخت — بدون تفکیک بخش
private bool $bookable = false; // نمایش در نوبت‌دهی
private Collection $staffMembers; // ManyToMany به ClinicStaff
private ?int $inventoryPackageId = null;
private Collection $consumables; // ServiceItemConsumable
```
+ `Tariff` (قیمت سالانه per سرویس) و `TenantServiceCoverage` (پوشش بیمه).
| مستند | وضعیت |
|---|---|
| دسته‌بندی درختی خدمات | ⚠️ `ServiceSection` تک‌سطحی است، درختی نیست |
| گروه آیتم با حداقل/حداکثر انتخاب | ❌ |
| آیتم با «زمان تنها» و «زمان اضافه» | ❌ — فقط یک `duration_minutes` |
| ناسازگاری / پیش‌نیاز بین آیتم‌ها | ❌ |
| الگوی بخش‌های نوبت (segment template) | ❌ |
| قیمت اختصاصی شعبه | ❌ |
| تک‌جلسه یا دوره‌ای | ❌ |
### ۲-۳ منابع
تنها «منبع» مدل‌شده، پرسنل است:
[src/Staff/Entity/ClinicStaff.php](../../../src/Staff/Entity/ClinicStaff.php) — نام، سمت، فعال/غیرفعال،
اتصال اختیاری به `User`. تقویم ندارد، ظرفیت ندارد، مهارت ندارد.
| مستند | وضعیت |
|---|---|
| `resource_type` تعریف‌شده توسط کلینیک | ❌ |
| منبع با ظرفیت همزمان | ❌ |
| مهارت‌ها (`skill` / `resource_skill`) | ❌ |
| استخر منابع | ❌ |
| ویژگی آزاد (جنسیت، مدل دستگاه، طبقه) | ❌ |
| زمان آماده‌سازی/تمیزکاری per منبع | ❌ (فقط `buffer_minutes` سراسری روی برنامه) |
| نیازمندی منبع per بخش | ❌ |
| قید هم‌جنس بودن | ❌ |
### ۲-۴ تقویم و اسلات
[src/Appointment/Entity/WeeklySchedule.php](../../../src/Appointment/Entity/WeeklySchedule.php):
JSON هفتگی per `(doctor, clinic)`، هر روز چند `session` با
`start_time/end_time/duration_per_patient/has_rest/patient_limit/location_id`.
`meta`: `online_booking_enabled`, `booking_window_value|unit`, `booking_mode`, `buffer_minutes`.
`DateOverride` (روز خاص)، `Holiday` (بازه تعطیلی per پزشک/محیط).
کسر لایه‌ها در [SlotCalculatorService](../../../src/Appointment/Service/SlotCalculatorService.php)
انجام می‌شود و از هفت لایهٔ مستند، چهار لایه را دارد:
```
ساعت کاری شعبه ❌ (ساعت کاری فقط روی برنامه پزشک است)
– شیفت منبع ❌
– تعطیلات رسمی کشور ❌ (جدول تعطیلات ملی نداریم؛ Holiday دستی است)
– مرخصی/غیبت ⚠️ فقط از راه Holiday و DateOverride پزشک
– سرویس دوره‌ای دستگاه ❌
– نوبت‌های ثبت‌شده ✅ isSlotTaken / findBusyIntervals
– رزروهای موقت ✅ pending با expires_at
– آماده‌سازی و تمیزکاری ⚠️ فقط buffer_minutes ثابت
```
### ۲-۵ نوبت‌دهی سرویسی که امروز داریم
جریان فعلی (همانی که کاربر اشاره کرد):
1. `GET /api/v1/appointment-booking-services/{doctorUuid}``booking_mode` + سرویس‌های `bookable`
2. `GET /api/v1/appointment-service-slots?doctor_uuid&date&service_item_uuids[]&durations[]`
`SlotCalculatorService::getServiceStartTimes()`**جمع سادهٔ مدت سرویس‌ها**، سپس پر کردن
فضای خالی هر شیفت با `duration + buffer`
3. `POST /api/v1/appointment` → یک ردیف `appointments` با `slot_start/slot_end` و
`appointment_service_items` (ManyToMany چند سرویس)
محدودیت‌های ساختاری این جریان نسبت به مستند:
- **`$totalMinutes += $duration` برای هر سرویس** ([AppointmentController.php:236](../../../src/Appointment/Controller/AppointmentController.php)) —
دقیقاً همان «فرمول قدیمی» که مستند بند ۵ ردش می‌کند: آماده‌سازی چند بار حساب می‌شود.
- زمان اشغال یک بلوک پیوسته است؛ اپراتور در زمان انتظار آزاد نمی‌شود (بند ۷).
- تنها منبعی که تداخلش بررسی می‌شود پزشک است؛ اگر دو سرویس هم‌زمان به یک پرسنل
یا یک دستگاه نیاز داشته باشند، سیستم متوجه نمی‌شود.
### ۲-۶ ثبت نوبت و همزمانی
[src/Appointment/Entity/Appointment.php](../../../src/Appointment/Entity/Appointment.php):
```php
public const PAYMENT_TTL = 900; // رزرو موقت ۱۵ دقیقه‌ای
#[ORM\Column(name:'active_slot_key', unique:true, nullable:true)]
private ?string $activeSlotKey = null; // "{doctorId}:{slotStart}" یا NULL
#[ORM\Version] private int $version = 1; // optimistic locking
```
✅ سه مرحله جستجو → رزرو موقت (`pending` + `expires_at`) → ثبت نهایی (`confirmed`) از قبل هست،
و یکتایی در سطح دیتابیس تضمین می‌شود — نه در کد.
❌ ولی کلید فقط `doctor + slot_start` است. با چند منبع، به یک جدول `resource_occupancy`
با محدودیت بازه‌ای نیاز است.
وضعیت‌ها: `pending, confirmed, completed, cancelled_by_doctor, cancelled_by_user, expired,
no_show, following_up, salon` + `AppointmentEvent` برای تاریخچه. تقریباً کامل؛ `rescheduled` ندارد.
### ۲-۷ قیمت
`ServiceItem.price_rials``Tariff` (سالانه) → `TenantServiceCoverage`/`TenantInsurance` (بیمه)
`DiscountRule` + `DiscountEngine``Invoice`/`InvoiceItem``Payment`.
بیعانه هم روی نوبت هست (`deposit_required`, `deposit_amount_rials`).
| مستند | وضعیت |
|---|---|
| لیست قیمت با بازهٔ تاریخ | ⚠️ `Tariff` فقط «سال» دارد، بازهٔ دقیق ندارد |
| قیمت per شعبه | ❌ |
| snapshot فاکتور روی نوبت | ⚠️ `visit_price_rials` تک‌عدد است، تفکیک‌شده نیست |
| پکیج و دفتر اعتبار جلسات | ❌ |
| بیعانه | ✅ |
### ۲-۸ قوانین
تنها موتور قانونِ موجود `DiscountRule` است
([src/Discount/Entity/DiscountRule.php](../../../src/Discount/Entity/DiscountRule.php)):
`type` از یک enum بسته، `priority`، `combinable`، `valid_from/valid_to`، tenant-دار.
این دقیقاً الگوی درستی است که مستند می‌خواهد (شرط از فهرست بسته، نه کد دلخواه) — ولی
فقط برای دستهٔ «قیمت». پنج دستهٔ دیگر (انتخاب، صلاحیت بیمار، منبع، زمان، فاصله زمانی)
و همچنین نسخه‌بندی و محیط آزمایش وجود ندارند.
### ۲-۹ دوره درمان
❌ کامل غایب. نه `course_protocol`، نه `treatment_course`، نه `course_session`.
`PatientSession` وجود دارد ولی «مراجعهٔ انجام‌شده» است، نه جلسهٔ برنامه‌ریزی‌شدهٔ یک دوره.
---
## ۳. جدول شکاف (خلاصه)
| بخش مستند | دارد | ندارد | تسک |
|---|---|---|---|
| ۴ کلینیک/شعبه/اتاق | tenant دوسطحی | Branch، Room، ساعت کاری شعبه | ۰۱ |
| ۵ تعریف خدمات | سرویس، قیمت، مدت، بیمه | گروه آیتم، دو نوع زمان، ناسازگاری، override شعبه | ۰۴ |
| ۶ منابع | پرسنل بدون تقویم | نوع منبع، ظرفیت، مهارت، استخر، نیازمندی | ۰۲، ۰۳ |
| ۷ بخش‌های نوبت | — | کل بخش | ۰۵ |
| ۸ قوانین | فقط تخفیف | ۵ دستهٔ دیگر، نسخه‌بندی، sandbox | ۰۹، ۱۰ |
| ۹ تقویم | برنامهٔ پزشک، override، تعطیلی | تقویم منبع، تعطیلات ملی، سرویس دستگاه | ۰۳ |
| ۱۰ جستجوی وقت | تک‌منبعی و پیوسته | چندمنبعی، چندبخشی، کش، استراتژی انتخاب | ۰۶ |
| ۱۱ ثبت نوبت | سه‌مرحله‌ای + یکتایی DB | resource_occupancy، قفل چندمنبعی | ۰۷ |
| ۱۲ قیمت | تعرفه، بیمه، تخفیف، بیعانه | price_list بازه‌دار، snapshot تفکیک‌شده، پکیج، دفتر اعتبار | ۰۸، ۱۱ |
| ۱۳ دوره درمان | — | کل بخش | ۱۲ |
| ۱۶ رویدادها | AppointmentEvent | bus عمومی دامنه | ۱۴ |
---
## ۴. تصمیم معماری پیشنهادی: توسعه، نه بازنویسی
مستند PostgreSQL و یک سیستم نو فرض کرده. پروژه روی **MariaDB 11.8 + Doctrine ORM 3.6**
است و یک جریان نوبت‌دهی زنده دارد (سایت عمومی `nobat724_front` و اپ `clinic-pro-tauri`
هر دو مصرف‌کنندهٔ `/api/v1/appointment*` هستند). پس:
1. **`Doctor` را به یک `Resource` تبدیل نمی‌کنیم، بلکه کنارش می‌گذاریم.**
پزشک منبعی با `resource_type = doctor` می‌شود که به رکورد `Doctor` لینک دارد.
`appointments.doctor_id` سر جایش می‌ماند تا API عمومی نشکند.
2. **حالت سوم نوبت‌دهی اضافه می‌شود:** `WeeklySchedule.meta.booking_mode = resource`
کنار `slot` و `service` موجود. دو حالت قبلی دست‌نخورده کار می‌کنند و مسیر مهاجرت
داوطلبانه است، نه اجباری.
3. **`resource_occupancy` تنها مرجع اشغال می‌شود** ولی `active_slot_key` فعلی هم تا
حذف کامل حالت `slot` می‌ماند (دو تور ایمنی، نه صفر).
4. **قوانین روی الگوی `DiscountRule` ساخته می‌شوند** — enum بسته + priority + بازهٔ اعتبار،
نه DSL آزاد. همان‌طور که مستند بند ۸ اصرار دارد.
5. **همهٔ جدول‌های جدید از روز اول `TenantOwnedTrait` می‌گیرند**، وگرنه
`TenantSchemaCoverageTest` قرمز می‌شود.
6. **MariaDB محدودیت بازه‌ای (`EXCLUDE`) ندارد.** جلوگیری از تداخل با
کلید یکتای «سطل زمانی» (`resource_id + slot_bucket`) انجام می‌شود — جزئیات در تسک ۰۷.
---
## ۵. ترتیب اجرا
```
۰۱ شعبه/اتاق ─┬─ ۰۲ منابع و مهارت ── ۰۳ تقویم منبع ─┐
└─ ۰۴ کاتالوگ خدمات v2 ── ۰۵ بخش‌های نوبت ─┴─ ۰۶ جستجوی وقت ── ۰۷ رزرو و ثبت
۰۸ قیمت‌گذاری و snapshot ────────────────┘
۰۹ موتور قوانین ── ۱۰ فرم و sandbox قانون
۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدم‌حضور/لیست انتظار
۱۴ رویدادها و گزارش بهره‌وری
```