Files
clinicpro/docs/new_feture/taskes/00-current-state-report.md
T
hamed 021d0eb6b2 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.
2026-07-30 11:43:58 +03:30

236 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# گزارش وضعیت فعلی سیستم در برابر مستند «موتور نوبت‌دهی 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 قانون
۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدم‌حضور/لیست انتظار
۱۴ رویدادها و گزارش بهره‌وری
```