Files
clinicpro/docs/new_feture/taskes/00-current-state-report.md
hamed 70739691d1 Add checklists for tasks 11 to 14 covering credit ledger, treatment course, cancellation policies, and event utilization
- Created checklist for task 11: Package and Credit Ledger
- Created checklist for task 12: Treatment Course
- Created checklist for task 13: Cancellation Policy, No-Show, and Waitlist
- Created checklist for task 14: Domain Events and Utilization Reports
2026-07-30 12:12:45 +03:30

268 lines
18 KiB
Markdown
Raw Permalink 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)) —
دقیقاً همان «فرمول قدیمی» که مستند بند ۵ ردش می‌کند: آماده‌سازی چند بار حساب می‌شود.
- زمان اشغال یک بلوک پیوسته است؛ اپراتور در زمان انتظار آزاد نمی‌شود (بند ۷).
- تنها منبعی که تداخلش بررسی می‌شود پزشک است؛ اگر دو سرویس هم‌زمان به یک پرسنل
یا یک دستگاه نیاز داشته باشند، سیستم متوجه نمی‌شود.
### ۲-۵ب حالت سرویسی **نیمه‌کاره** است — پنج شکاف در چرخهٔ عمر نوبت
مسیر **رزرو** کار می‌کند، ولی بقیهٔ چرخهٔ عمر نه. این‌ها پیش‌نیاز موتور چندمنبعی‌اند و
تسک‌های [۰۰](task-00-service-mode-completion/) و [۰۰ب](task-00b-nobat724-service-mode/)
می‌بندندشان:
| # | شکاف | محل |
|---|---|---|
| ۱ | `PATCH /appointment/{uuid}` مدت دلخواه می‌پذیرد؛ بافر را نادیده می‌گیرد؛ فقط `service_item_uuid` تکی را به‌روز می‌کند در حالی که `service_items` (ManyToMany) دست‌نخورده می‌ماند | [AppointmentController.php:1077](../../../src/Appointment/Controller/AppointmentController.php) |
| ۲ | `AppointmentEditPage` سه فیلد آزاد `date`/`start`/`end` دارد و هیچ `ServiceSlotPicker` ای ندارد — منشی نوبت ۴۵ دقیقه‌ای را ۲۰ دقیقه می‌کند و سیستم قبول می‌کند | [AppointmentEditPage.tsx:74](../../../assets/admin/pages/AppointmentEditPage.tsx) |
| ۳ | نوبت رزرو (`is_reserve`) صریحاً از حالت سرویسی حذف شده (`serviceMode = mode === 'service' && !isReserve`) و مسیر تبدیل رزرو به نوبت سرویسی وجود ندارد | [NewAppointmentDrawer.tsx:72](../../../assets/admin/components/NewAppointmentDrawer.tsx) |
| ۴ | سایت عمومی چهار رنگ hard-code در مرحلهٔ انتخاب سرویس دارد (`#5559CE`, `#3B3B3B`, `#7A7A7A`, `bg-white`) و در دارک‌مود می‌شکند؛ همچنین مدت را **موازی با بک‌اند** حساب می‌کند | `nobat724_front/components/appointment/service/index.js` |
| ۵ | پنل کاربر سایت نام سرویس و مدت نوبت را نشان نمی‌دهد و مسیر جابه‌جایی سرویس‌آگاه ندارد | `nobat724_front/.../turns/Card.js` · `isTurnsDetails/*` |
نکتهٔ ۴ دو مشکل در یک فایل است: انحراف از دیزاین‌سیستم، و منبع دوم حقیقت برای مدت.
دومی مهم‌تر است — وقتی تسک ۰۴ فرمول را به «زمان تنها / زمان اضافه» عوض کند، سایت عدد
قدیمی نشان می‌دهد و بیمار مدتی می‌بیند که با مدت واقعی نوبتش نمی‌خواند.
### ۲-۶ ثبت نوبت و همزمانی
[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`) انجام می‌شود — جزئیات در تسک ۰۷.
---
## ۵. ترتیب اجرا
```
۰۰ تکمیل سرویسی (clinicpro) ── ۰۰ب سازگارسازی سایت ← فاز ۰، پیش‌نیاز بقیه
├─ ۰۱ شعبه/اتاق ─┬─ ۰۲ منابع و مهارت ── ۰۳ تقویم منبع ─┐
│ └─ ۰۴ کاتالوگ v2 ── ۰۵ بخش‌های نوبت ─┴─ ۰۶ جستجوی وقت ── ۰۷ رزرو و ثبت
│ │
│ ۰۸ قیمت‌گذاری و snapshot ───────────────┘
│ │
│ ۰۹ موتور قوانین ── ۱۰ فرم و sandbox قانون
│ │
│ ۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدم‌حضور/انتظار
│ │
└──────────────────────── ۱۴ رویدادها و گزارش بهره‌وری
```
**فاز ۰ اختیاری نیست.** اگر حالت `resource` روی حالت `service` نیمه‌کاره ساخته شود، هر
باگ موجود سرویسی به موتور جدید ارث می‌رسد و تشخیص منبعش غیرممکن می‌شود.
## ۶. سه قاعدهٔ حاکم بر همهٔ تسک‌ها
| سند | چه می‌گوید |
|---|---|
| [_shared/red-lines.md](_shared/red-lines.md) | منطق اسلاتی به هیچ عنوان دست‌کاری نمی‌شود · فهرست کامل فایل‌های قفل‌شده · تست `--group=slot-mode-frozen` |
| [_shared/ui-conventions.md](_shared/ui-conventions.md) | هر صفحه یا بخش جدید عیناً با دیزاین‌سیستم موجود — توکن‌ها، کامپوننت‌های `ui/`، پنج قاعدهٔ غیرقابل‌مذاکره |
| [_shared/definition-of-done.md](_shared/definition-of-done.md) | هیچ تسکی بدون تکمیل چک‌لیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |