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
This commit is contained in:
hamed
2026-07-30 12:12:45 +03:30
parent 158dcb58aa
commit 70739691d1
16 changed files with 1726 additions and 53 deletions
@@ -128,6 +128,24 @@ JSON هفتگی per `(doctor, clinic)`، هر روز چند `session` با
- تنها منبعی که تداخلش بررسی می‌شود پزشک است؛ اگر دو سرویس هم‌زمان به یک پرسنل
یا یک دستگاه نیاز داشته باشند، سیستم متوجه نمی‌شود.
### ۲-۵ب حالت سرویسی **نیمه‌کاره** است — پنج شکاف در چرخهٔ عمر نوبت
مسیر **رزرو** کار می‌کند، ولی بقیهٔ چرخهٔ عمر نه. این‌ها پیش‌نیاز موتور چندمنبعی‌اند و
تسک‌های [۰۰](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):
@@ -182,6 +200,7 @@ no_show, following_up, salon` + `AppointmentEvent` برای تاریخچه. تق
| بخش مستند | دارد | ندارد | تسک |
|---|---|---|---|
| — نوبت‌دهی سرویسی موجود | مسیر رزرو (پنل + سایت) | ویرایش، جابه‌جایی، رزرو، پنل بیمار، دیزاین‌سیستم سایت | **۰۰، ۰۰ب** |
| ۴ کلینیک/شعبه/اتاق | tenant دوسطحی | Branch، Room، ساعت کاری شعبه | ۰۱ |
| ۵ تعریف خدمات | سرویس، قیمت، مدت، بیمه | گروه آیتم، دو نوع زمان، ناسازگاری، override شعبه | ۰۴ |
| ۶ منابع | پرسنل بدون تقویم | نوع منبع، ظرفیت، مهارت، استخر، نیازمندی | ۰۲، ۰۳ |
@@ -222,14 +241,27 @@ no_show, following_up, salon` + `AppointmentEvent` برای تاریخچه. تق
## ۵. ترتیب اجرا
```
۰۱ شعبه/اتاق۰۲ منابع و مهارت ── ۰۳ تقویم منبع ─┐
└─ ۰۴ کاتالوگ خدمات v2 ── ۰۵ بخش‌های نوبت ─┴─ ۰۶ جستجوی وقت ── ۰۷ رزرو و ثبت
۰۸ قیمت‌گذاری و snapshot ────────────────┘
۰۹ موتور قوانین ── ۱۰ فرم و sandbox قانون
۱۱ پکیج و دفتر اعتبار ── ۱۲ دوره درمان ── ۱۳ لغو/عدم‌حضور/لیست انتظار
۱۴ رویدادها و گزارش بهره‌وری
۰۰ تکمیل سرویسی (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) | هیچ تسکی بدون تکمیل چک‌لیستش تمام نیست — ✅ 🔄 ⏳ ⚠️ |
+117 -43
View File
@@ -3,17 +3,34 @@
پیاده‌سازی تدریجی [clinic-pro-mostanad-sade.md](../clinic-pro-mostanad-sade.md) روی کد موجود.
گزارش وضعیت فعلی و تحلیل شکاف: [00-current-state-report.md](00-current-state-report.md)
> **پیش‌فرض کلیدی:** بازنویسی نداریم. نوبت‌دهی اسلاتی (`booking_mode=slot`) و نوبت‌دهی
> سرویسیِ فعلی (`booking_mode=service`) تا آخر این مسیر بدون تغییر رفتار کار می‌کنند.
> حالت جدید `booking_mode=resource` کنارشان اضافه می‌شود.
---
## ⛔ سه قاعده‌ای که پیش از هر تسکی باید بخوانی
| سند | چه می‌گوید |
|---|---|
| [_shared/red-lines.md](_shared/red-lines.md) | **منطق اسلاتی به هیچ عنوان دست‌کاری نمی‌شود** · نوبت‌دهی سرویسی در همین فاز کامل می‌شود |
| [_shared/ui-conventions.md](_shared/ui-conventions.md) | **هر صفحه یا بخش جدید عیناً با دیزاین‌سیستم موجود** — هیچ طراحی جدید |
| [_shared/definition-of-done.md](_shared/definition-of-done.md) | **هیچ تسکی بدون تکمیل چک‌لیستش تمام نیست** — ✅ 🔄 ⏳ ⚠️ |
در تناقض، این سه سند بر متن تسک‌ها برنده‌اند.
---
## لیست تسک‌ها
| تسک | ماژول | Endpoint جدید | وابستگی | زمان |
|-----|-------|--------------|---------|------|
| [۰۱](task-01-branch-room/) | شعبه و اتاق | ۸ | — | ۱۰-۱۲h |
### فاز ۰ — تثبیت وضعیت فعلی (پیش‌نیاز بقیه)
| تسک | ماژول | پروژه | Endpoint | وابستگی | زمان |
|-----|-------|-------|----------|---------|------|
| [۰۰](task-00-service-mode-completion/) | تکمیل نوبت‌دهی سرویسی | `clinicpro` | ۴ | — | ۱۴-۱۸h |
| [۰۰ب](task-00b-nobat724-service-mode/) | سازگارسازی سایت عمومی | `nobat724_front` | — | ۰۰ | ۱۰-۱۴h |
### فاز ۱ — هستهٔ چندمنبعی
| تسک | ماژول | Endpoint | وابستگی | زمان |
|-----|-------|----------|---------|------|
| [۰۱](task-01-branch-room/) | شعبه و اتاق | ۸ | ۰۰ | ۱۰-۱۲h |
| [۰۲](task-02-resource-model/) | منبع، نوع منبع، مهارت، استخر | ۱۴ | ۰۱ | ۱۴-۱۸h |
| [۰۳](task-03-resource-calendar/) | تقویم منبع، مرخصی، تعطیلات ملی | ۹ | ۰۱، ۰۲ | ۱۲-۱۴h |
| [۰۴](task-04-service-catalog-v2/) | کاتالوگ خدمات v2 (گروه آیتم، دو نوع زمان) | ۱۰ | ۰۱ | ۱۴-۱۶h |
@@ -21,14 +38,29 @@
| [۰۶](task-06-availability-engine/) | موتور جستجوی وقت چندمنبعی | ۲ | ۰۳، ۰۵ | ۲۰-۲۴h |
| [۰۷](task-07-hold-and-book/) | رزرو موقت و ثبت نهایی چندمنبعی | ۴ | ۰۶ | ۱۶-۲۰h |
| [۰۸](task-08-pricing-snapshot/) | لیست قیمت بازه‌دار و snapshot فاکتور | ۷ | ۰۴، ۰۷ | ۱۲-۱۴h |
### فاز ۲ — قوانین
| تسک | ماژول | Endpoint | وابستگی | زمان |
|-----|-------|----------|---------|------|
| [۰۹](task-09-policy-engine/) | موتور قوانین شش‌دسته‌ای | ۶ | ۰۵، ۰۶، ۰۸ | ۲۰-۲۴h |
| [۱۰](task-10-policy-admin-sandbox/) | فرم ساخت قانون + محیط آزمایش | ۲ | ۰۹ | ۱۰-۱۲h |
### فاز ۳ — کسب‌وکار
| تسک | ماژول | Endpoint | وابستگی | زمان |
|-----|-------|----------|---------|------|
| [۱۱](task-11-package-credit-ledger/) | پکیج و دفتر اعتبار جلسات | ۸ | ۰۸ | ۱۰-۱۲h |
| [۱۲](task-12-treatment-course/) | دوره درمان | ۹ | ۰۷، ۱۱ | ۱۶-۲۰h |
| [۱۳](task-13-cancellation-waitlist/) | سیاست لغو، عدم حضور، لیست انتظار | ۷ | ۰۷ | ۱۰-۱۲h |
### فاز ۴ — بهینه‌سازی
| تسک | ماژول | Endpoint | وابستگی | زمان |
|-----|-------|----------|---------|------|
| [۱۴](task-14-events-utilization/) | رویدادهای دامنه و گزارش بهره‌وری | ۳ | ۰۷ | ۸-۱۰h |
**مجموع endpoint جدید: ~۹۲ · مجموع زمان: ۱۹۰ تا ۲۲۸ ساعت**
**مجموع endpoint جدید: ~۹۶ · مجموع زمان: ۲۱۴ تا ۲۵۶ ساعت**
---
@@ -37,57 +69,99 @@
```
task-XX-name/
├── task.md ← شرح، دامنه، endpoint ها، معیار پذیرش، زمان
├── architecture.md ← فایل‌ها، entity ها، سرویس‌ها، لایه‌ها
├── architecture.md ← فایل‌ها، entity ها، سرویس‌ها، لایه‌ها، قواعد UI
├── database.md ← جداول، ستون‌ها، ایندکس‌ها، migration
├── implementation_notes.md ← نکات فنی، edge case، سازگاری عقب‌رو، تست
├── checklist.md ← ☑️ وضعیت هر مورد: ✅ 🔄 ⏳ ⚠️
└── user_flow.md ← (تسک‌های پیچیده) جریان کاربری
```
`checklist.md` **اجباری** است و ساختار ثابتی دارد:
```
۰. خط سرخ‌ها ۱. بک‌اند ۲. دیتابیس ۳. UI ۴. تست ۵. مستندات ۶. بازبینی پایانی
```
---
## چک‌لیست — قواعد
| نماد | معنی | اجازهٔ باقی‌ماندن در پایان تسک |
|---|---|---|
| ✅ | انجام‌شده و تأییدشده | بله |
| 🔄 | در حال انجام | **نه** |
| ⏳ | انجام‌نشده | **نه** — مگر با دلیل مکتوب و تسک مقصد |
| ⚠️ | نیازمند بررسی یا تست | **نه** — باید تعیین تکلیف شود |
**پیش از پایان هر تسک، همهٔ ردیف‌های چک‌لیست بازبینی می‌شوند و وضعیت نهایی می‌گیرند.**
هر تسک بخش «۶. بازبینی پایانی» دارد که تست‌ها، مستندات، UI و دو کلاینت دیگر را می‌سنجد.
---
## ترتیب پیشنهادی اجرا
```
۰۱۰۲ ── ۰۳ ─┐
└─ ۰۴ ── ۰۵ ─┴─ ۰۶ ── ۰۷ ─┬─ ۰۸ ─┬─ ۰۹ ── ۱۰
│ └─ ۱۱ ── ۱۲
۱۳
└─ ۱۴
۰۰ ── ۰۰ب
├─ ۰۱ ─┬─ ۰۲ ── ۰۳ ─┐
└─ ۰۴ ── ۰۵ ─┴─ ۰۶ ── ۰۷ ─┬─ ۰۸ ─┬─ ۰۹ ─۱۰
│ │ └─ ۱۱ ── ۱۲
│ ├─ ۱۳
│ └─ ۱۴
```
فاز اول (هستهٔ قابل عرضه): ۰۱ تا ۰۸ — بعد از آن یک کلینیک زیبایی با اتاق، دستگاه و
اپراتور می‌تواند واقعاً نوبت بگیرد.
**فاز ۰ اختیاری نیست.** اگر حالت `resource` (تسک ۰۶) روی حالت `service` نیمه‌کاره ساخته
شود، هر باگ موجود سرویسی به موتور جدید ارث می‌رسد و تشخیص منبعش غیرممکن می‌شود.
فاز اول قابل عرضه: ۰۰ تا ۰۸ — بعد از آن یک کلینیک زیبایی با اتاق، دستگاه و اپراتور
می‌تواند واقعاً نوبت بگیرد.
---
## سه حالت نوبت‌دهی
| حالت | مقدار `WeeklySchedule.meta.booking_mode` | وضعیت |
|---|---|---|
| اسلاتی | `slot` | ⛔ **قفل** — پیش‌فرض، تولیدی، دست‌نخورده |
| سرویسی | `service` | 🔄 موجود ولی نیمه‌کاره → تسک ۰۰ و ۰۰ب کاملش می‌کنند |
| چندمنبعی | `resource` | ⏳ جدید — تسک ۰۶ به بعد |
`booking_mode` پس از اولین ثبت قفل می‌شود (`WeeklySchedule::getStoredBookingMode()`).
تسک ۰۶ یک استثنای کنترل‌شده اضافه می‌کند: ارتقای **یک‌طرفه** از `slot`/`service` به
`resource`، مشروط بر نبودِ نوبت فعال آینده. بازگشت ممنوع.
ماتریس کامل «کدام endpoint در کدام حالت» در `docs/architecture/booking-modes.md`
(تسک ۰۰ می‌سازد، تسک ۰۶ حالت سوم را اضافه می‌کند).
---
## اجبار خودکار خط سرخ
هر تسک باید این را سبز نگه دارد:
```bash
ddev exec php bin/phpunit --group=slot-mode-frozen
```
تسک ۰۰ این تست و سه fixture آن را می‌سازد. fixture ها بعد از آن **read-only** اند:
اگر تستی قرمز شد، **کد باید برگردد، نه fixture**.
---
## قواعد مشترک همهٔ تسک‌ها
قواعد پروژه در [CLAUDE.md](../../../CLAUDE.md) و
[docs/architecture/tenancy.md](../../architecture/tenancy.md) بر همهٔ این تسک‌ها حاکم‌اند.
مواردی که در هر تسک باید رعایت شوند:
از [CLAUDE.md](../../../CLAUDE.md) و [docs/architecture/tenancy.md](../../architecture/tenancy.md).
فهرست کامل در [_shared/definition-of-done.md](_shared/definition-of-done.md):
1. هر entity جدید یا `TenantOwnedTrait` می‌گیرد یا در `GlobalTables` با دلیل ثبت می‌شود؛
`TenantSchemaCoverageTest` را اجرا کن.
2. `entity_type, entity_id` ستون‌های **اول** هر ایندکس ترکیبی لیست.
3. هر uuid که از request می‌آید باید با `TenantOwnershipChecker` سنجیده شود؛
`TenantLookupInventoryTest` شمارنده دارد.
4. timestamp ها `int` (Unix)، نه `DateTime`. نمایش شمسی فقط در UI.
5. کنترلر نازک، `extends BaseController`، پاسخ با `success()/paginated()/error()`.
6. هر endpoint جدید یا تغییر یافته → به‌روزرسانی `docs/api/*.md` در همان نشست.
7. تست موفق + خطا + مرزی برای هر تسک، وگرنه تسک تمام نیست.
8. رشته‌های UI فارسی، کد و کامیت انگلیسی.
9. سازگاری عقب‌رو: `nobat724_front` و `clinic-pro-tauri` مصرف‌کنندهٔ همین APIها هستند
و در build خطا نمی‌دهند — هر تغییر قرارداد باید دستی بررسی شود.
---
## سازگاری با نوبت‌دهی فعلی
| حالت | منبع تنظیم | چه زمانی |
|---|---|---|
| `slot` | `WeeklySchedule.meta.booking_mode` | اسلات ثابت `duration_per_patient` — رفتار پیش‌فرض امروز |
| `service` | همان | طول = جمع مدت سرویس‌ها + buffer — پیاده‌شده، تک‌منبعی |
| `resource` | همان | **جدید** — برنامهٔ چندبخشی + چند منبع (تسک ۰۵ به بعد) |
`booking_mode` پس از اولین ثبت قفل می‌شود (`WeeklySchedule::getStoredBookingMode()`).
تسک ۰۶ باید مسیر ارتقای داوطلبانهٔ `service → resource` را باز کند، بدون اجبار.
1. entity جدید یا `TenantOwnedTrait` می‌گیرد یا با دلیل در `GlobalTables` ثبت می‌شود
2. `entity_type, entity_id` ستون‌های **اول** هر ایندکس ترکیبیِ لیست
3. هر uuid از request با `TenantOwnershipChecker` سنجیده می‌شود
4. timestamp ها `int` یونیکس؛ نمایش شمسی فقط در UI
5. کنترلر نازک · `BaseController` · `success/paginated/error`
6. API جدید فقط وقتی هیچ endpoint موجودی کافی نباشد — **دلیلش نوشته شود**
7. تست موفق + خطا + مرزی؛ بدون اجرای موفق تست، تسک تمام نیست
8. هر endpoint جدید یا تغییریافته → `docs/api/*` در همان نشست
9. رشته‌های UI فارسی از i18n؛ کد و کامیت و مستندات انگلیسی
10. سازگاری عقب‌رو: `nobat724_front` و `clinic-pro-tauri` در build خطا نمی‌دهند —
بررسی دستی اجباری است (ردیف بازبینی پایانی هر تسک)
11. اول commit، بعد `graphify update .`
@@ -0,0 +1,92 @@
# چک‌لیست — تسک ۰۱ (شعبه و اتاق)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `SlotCalculatorService` دست‌نخورده | ⏳ | این تسک به آن کاری ندارد |
| ۰.۳ | `location_id` در JSON برنامهٔ هفتگی دست‌نخورده | ⏳ | شعبه **بالای** آدرس می‌نشیند |
| ۰.۴ | `DoctorAddress` هیچ ستونی حذف/تغییر نداد | ⏳ | فقط `branch_id` تهی‌پذیر اضافه شد |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `Branch` + `BranchWorkingHours` + `Room` entity | ⏳ | |
| ۱.۲ | `BranchService` با گاردهای حذف قابل توسعه (`DeletionGuardInterface`) | ⏳ | تسک ۰۲ و ۰۷ گارد اضافه می‌کنند |
| ۱.۳ | `WorkingHoursService` — اعتبارسنجی و `sequence` سمت سرور | ⏳ | |
| ۱.۴ | ساعت با `start_minute`/`end_minute` عددی، نه رشتهٔ `"09:00"` | ⏳ | |
| ۱.۵ | هشت endpoint ساخته شد | ⏳ | |
| ۱.۶ | پزشک مستقل هم شعبه دارد (مطب = شعبه) | ⏳ | نه فقط `entity_type=clinic` |
| ۱.۷ | `app:branch:backfill` — dry-run پیش‌فرض، idempotent | ⏳ | |
| ۱.۸ | کنترلر نازک · `BaseController` · `success/paginated/error` | ⏳ | |
| ۱.۹ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `branches` · `branch_working_hours` · `rooms` | ⏳ | |
| ۲.۲ | `entity_type, entity_id` ستون **اول** ایندکس‌های لیست | ⏳ | |
| ۲.۳ | `timezone` روی شعبه از روز اول | ⏳ | افزودن بعدی = backfill زمان‌دار |
| ۲.۴ | `rooms.capacity` — ظرفیت هم‌زمان | ⏳ | اتاق سه‌تخته = یک ردیف با ۳ |
| ۲.۵ | `branch_working_hours` در `GlobalTables::AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۶ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `BranchesPage` · `BranchFormPage` · `RoomsPage` | ⏳ | |
| ۳.۲ | `DataTable` با skeleton و empty state فارسی | ⏳ | |
| ۳.۳ | `PageHeader` با `backTo` روی زیرصفحه‌ها | ⏳ | |
| ۳.۴ | شهر/استان با `SearchableSelect` — هیچ `<select>` بومی | ⏳ | |
| ۳.۵ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
| ۳.۶ | هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکن | ⏳ | |
| ۳.۷ | دارک‌مود و حالت فشرده بررسی شد | ⏳ | |
| ۳.۸ | RTL و موبایل بررسی شد | ⏳ | |
| ۳.۹ | همهٔ رشته‌ها فارسی از i18n | ⏳ | |
| ۳.۱۰ | هشدار UI: «هیچ شعبهٔ فعالی باقی نمی‌ماند» | ⏳ | |
| ۳.۱۱ | مسیرها در `App.tsx` | ⏳ | |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `BranchCrudTest` — نقش‌ها، ۴۰۴ نه ۴۰۳ برای محیط دیگر | ⏳ | |
| ۴.۲ | `WorkingHoursTest``end<=start`، هم‌پوشانی، شبانه‌روزی `0..1440` | ⏳ | |
| ۴.۳ | `BranchDeletionTest` — شعبهٔ دارای اتاق فعال → ۴۲۲ | ⏳ | |
| ۴.۴ | `capacity=0` → ۴۲۲ | ⏳ | |
| ۴.۵ | `phpstan analyse src/Branch` بدون خطا | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/branch.md` + ثبت در `docs/api/README.md` | ⏳ | |
| ۵.۲ | تفسیر «شعبهٔ بدون ساعت کاری = تعریف‌نشده، نه همیشه‌باز» نوشته شد | ⏳ | تسک ۰۳ رویش حساب می‌کند |
| ۵.۳ | `docs/architecture/tenancy.md` جدول طبقه‌بندی به‌روز شد | ⏳ | |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | `nobat724_front` و `clinic-pro-tauri` بررسی شدند | ⏳ | این تسک قرارداد عمومی عوض نمی‌کند |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,100 @@
# چک‌لیست — تسک ۰۲ (منبع، نوع منبع، مهارت، استخر)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `Doctor` به `Resource` تبدیل **نشد** — فقط لینک شد | ⏳ | `appointments.doctor_id` سر جایش |
| ۰.۳ | `ClinicStaff` و `Room` هویت مستقل حفظ کردند | ⏳ | |
| ۰.۴ | `service_item_staff` و `ServiceItem.staffMembers` دست‌نخورده | ⏳ | |
| ۰.۵ | `WeeklySchedule.meta.buffer_minutes` دست‌نخورده | ⏳ | `setup/cleanup` مفهوم جداست |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `ResourceType` · `ClinicResource` · `Skill` · `ResourceSkill` | ⏳ | |
| ۱.۲ | `ResourcePool` · `ResourcePoolMember` | ⏳ | |
| ۱.۳ | نام کلاس `ClinicResource` (نه `Resource`) و جدول `clinic_resources` | ⏳ | |
| ۱.۴ | `ResourceLinker` — تنها نقطهٔ نگاشت پزشک/پرسنل/اتاق ↔ منبع | ⏳ | |
| ۱.۵ | حداکثر یکی از `doctor_id`/`staff_id`/`room_id` — قید در سازنده | ⏳ | |
| ۱.۶ | `capacity>1` روی `type=doctor` → ۴۲۲ | ⏳ | |
| ۱.۷ | `normalizeAttributes` — اسکالر، کلید `[a-z_]{1,40}`، سقف ۲۰ | ⏳ | |
| ۱.۸ | `ResourcePoolService` — اعضا هم‌شعبه و هم‌نوع، وگرنه ۴۲۲ | ⏳ | |
| ۱.۹ | `findEligible()` با `HAVING COUNT(DISTINCT skill) = n` | ⏳ | همهٔ مهارت‌ها، نه یکی |
| ۱.۱۰ | `StaffService` موجود `ResourceLinker::syncActive()` صدا می‌زند | ⏳ | نه lifecycle callback |
| ۱.۱۱ | چهارده endpoint | ⏳ | |
| ۱.۱۲ | `app:resource:backfill` — dry-run، idempotent | ⏳ | سه نوع سیستمی + پزشک/پرسنل/اتاق |
| ۱.۱۳ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | شش جدول ساخته شد | ⏳ | |
| ۲.۲ | سه UNIQUE تهی‌پذیر روی `doctor_id`/`staff_id`/`room_id` | ⏳ | |
| ۲.۳ | `idx_resource_skills_skill (skill_id, level)` | ⏳ | کوئری داغ تسک ۰۶ |
| ۲.۴ | `entity_type, entity_id` ستون اول ایندکس‌های لیست | ⏳ | |
| ۲.۵ | `resource_skills` و `resource_pool_members` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۶ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `ResourcesPage` · `ResourceFormPage` · `ResourceTypesPage` · `SkillsPage` · `ResourcePoolsPage` | ⏳ | |
| ۳.۲ | فیلتر شعبه/نوع/فعال در URL با `useUrlState` | ⏳ | |
| ۳.۳ | شعبه و نوع با `SearchableSelect` | ⏳ | |
| ۳.۴ | مهارت‌ها با چیپ چندانتخابی | ⏳ | |
| ۳.۵ | `DataTable` با skeleton و empty state | ⏳ | |
| ۳.۶ | `backTo`/`BackButton` روی همهٔ زیرصفحه‌ها | ⏳ | |
| ۳.۷ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۳.۸ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۹ | RTL و موبایل | ⏳ | |
| ۳.۱۰ | `setup/cleanup` با واحد فارسی «دقیقه» | ⏳ | |
| ۳.۱۱ | هشدار «این منبع N نوبت آیندهٔ فعال دارد» هنگام غیرفعال‌سازی | ⏳ | |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `ResourceCrudTest` — شعبهٔ محیط دیگر ۴۰۴، دو پل ۴۲۲، capacity | ⏳ | |
| ۴.۲ | `SkillAssignmentTest` — جایگزینی کامل، level، حذف در استفاده | ⏳ | |
| ۴.۳ | `ResourcePoolTest` — عضو شعبهٔ دیگر / نوع دیگر → ۴۲۲ | ⏳ | |
| ۴.۴ | `ResourceEligibilityTest` — دو مهارت، منبع با یکی برنمی‌گردد | ⏳ | |
| ۴.۵ | `BackfillResourceTest` — idempotent | ⏳ | |
| ۴.۶ | `TenantLookupInventoryTest` شمارنده به‌روز شد | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/resource.md` | ⏳ | |
| ۵.۲ | توضیح «ظرفیت ۳ = یک ردیف، نه سه منبع» | ⏳ | |
| ۵.۳ | تفاوت `setup/cleanup` منبع با `buffer_minutes` برنامه | ⏳ | |
| ۵.۴ | `docs/api/staff.md` بخش «رابطه با منبع» | ⏳ | |
| ۵.۵ | `docs/architecture/tenancy.md` به‌روز | ⏳ | |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | تست‌های tenant سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,113 @@
# چک‌لیست — تسک ۰۳ (تقویم منبع، استثنا، تعطیلات ملی)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `SlotCalculatorService` **هیچ** تغییری نکرد | ⏳ | `ResourceAvailabilityService` سرویس موازی است |
| ۰.۳ | `WeeklySchedule` · `DateOverride` · `Holiday` موجود دست‌نخورده | ⏳ | |
| ۰.۴ | `HolidayResolver` به مسیر اسلاتی **وصل نشد** | ⏳ | تغییر رفتاری است؛ تسک جدا با تأیید محصول |
## ۱. `IntervalSet` — اول از همه
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `src/Shared/Time/IntervalSet.php` ساخته شد **پیش از** entity ها | ⏳ | |
| ۱.۲ | قرارداد نیم‌باز `[start, end)` همه‌جا | ⏳ | |
| ۱.۳ | `IntervalSetTest` — هفت حالت مرزی سند | ⏳ | واحد، بدون DB |
| ۱.۴ | هر جای دیگری که بازه جمع/کم می‌کند از همین استفاده می‌کند | ⏳ | نه پیاده‌سازی دوم |
## ۲. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `ResourceCalendar` · `ResourceException` | ⏳ | |
| ۲.۲ | `NationalHoliday` · `TenantHolidayOverride` | ⏳ | |
| ۲.۳ | `ResourceAvailabilityService::rawWindows()` + `explainEmptyDay()` | ⏳ | پنج دلیل خالی‌بودن |
| ۲.۴ | `HolidayResolver::isClosedFor()` — تنها نقطهٔ ترکیب | ⏳ | |
| ۲.۵ | ترتیب تعطیلی: ملی + override محیط؛ منبع فقط می‌بندد نه باز می‌کند | ⏳ | |
| ۲.۶ | شعبهٔ بدون ساعت کاری = بی‌قید، **نه بسته** | ⏳ | همان تصمیم تسک ۰۱ |
| ۲.۷ | `union` استثناها بعد `subtract` — نه کسر پشت‌سرهم | ⏳ | |
| ۲.۸ | تبدیل روز هفته `(w+1)%7` عیناً مثل `SlotCalculatorService:359` | ⏳ | دو قرارداد = باگ قطعی |
| ۲.۹ | `DateTimeZone` صریح از `branch.timezone`، نه `date()` خالی | ⏳ | |
| ۲.۱۰ | نُه endpoint | ⏳ | |
| ۲.۱۱ | `app:holiday:import --year=` — idempotent | ⏳ | |
| ۲.۱۲ | `app:resource:calendar:backfill` از `WeeklySchedule` — dry-run | ⏳ | |
## ۳. کارایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `rawWindows` برای ۹۰ روز **دقیقاً ۵ کوئری** | ⏳ | |
| ۳.۲ | `AvailabilityQueryCountTest` عدد را قفل می‌کند | ⏳ | وگرنه اولین refactor می‌شکند |
| ۳.۳ | هیچ کوئری داخل حلقهٔ روز | ⏳ | |
| ۳.۴ | `idx_rex_resource_range` ساخته شد | ⏳ | |
## ۴. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | چهار جدول | ⏳ | |
| ۴.۲ | `valid_from`/`valid_to` روی شیفت از روز اول | ⏳ | شیفت فصلی |
| ۴.۳ | نوع `blocked` در استثناها هست | ⏳ | تفکیک از مرخصی برای گزارش تسک ۱۴ |
| ۴.۴ | `national_holidays` در `GlobalTables::ENTITIES` با دلیل | ⏳ | |
| ۴.۵ | `resource_calendars` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۴.۶ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۵. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `ResourceCalendarPage` · `ResourceExceptionsPage` · `HolidaysSettingsPage` | ⏳ | |
| ۵.۲ | گرید هفت‌روزه با فرم ساده (بدون drag) | ⏳ | |
| ۵.۳ | بازهٔ تاریخ با `PersianDatePicker` | ⏳ | |
| ۵.۴ | `backTo`/`BackButton` روی هر سه | ⏳ | |
| ۵.۵ | تعطیلات سال با تیک «ما این روز کار می‌کنیم» | ⏳ | |
| ۵.۶ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۵.۷ | دارک‌مود و حالت فشرده | ⏳ | |
| ۵.۸ | RTL و موبایل | ⏳ | |
| ۵.۹ | دلیل خالی‌بودن روز با پیام فارسی نمایش داده می‌شود | ⏳ | پنج حالت |
## ۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `IntervalSetTest` | ⏳ | |
| ۶.۲ | `ResourceCalendarTest``end<=start`، هم‌پوشانی، شیفت شبانه → ۴۲۲ | ⏳ | |
| ۶.۳ | `ResourceAvailabilityTest` — تقاطع، شعبهٔ بی‌ساعت، نیم‌روزه، دو استثنای هم‌پوشان | ⏳ | |
| ۶.۴ | `AvailabilityQueryCountTest` | ⏳ | |
| ۶.۵ | `HolidayResolverTest` — ملی، override دو جهت | ⏳ | |
| ۶.۶ | `ImportNationalHolidaysTest` — idempotent | ⏳ | |
| ۶.۷ | بازهٔ > ۹۰ روز → ۴۲۲ | ⏳ | |
## ۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `docs/api/resource-calendar.md` | ⏳ | |
| ۷.۲ | `docs/api/appointment-settings.md` بخش «تفاوت با تقویم منبع» | ⏳ | دو سیستم قاطی نشوند |
| ۷.۳ | تصمیم «شیفت شبانه پشتیبانی نمی‌شود» مکتوب | ⏳ | |
## ۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۸.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۸.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۸.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۸.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۸.۶ | تست‌های tenant سبز | ⏳ | |
| ۸.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۸.۸ | چک‌لیست UI کامل | ⏳ | |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | |
| ۸.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | اتصال `HolidayResolver` به مسیر اسلاتی |
@@ -0,0 +1,111 @@
# چک‌لیست — تسک ۰۴ (کاتالوگ خدمات v2)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | نام `ServiceItem` عوض **نشد** | ⏳ | هفت جدول + سه ریپو رویش‌اند |
| ۰.۳ | `service_items.duration_minutes` حذف نشد | ⏳ | مدت پایهٔ سرویس می‌ماند |
| ۰.۴ | `ServiceSection` (بخش کلینیک) دست‌نخورده | ⏳ | مفهومش با دسته‌بندی فرق دارد |
| ۰.۵ | `BackwardCompatibilityTest`: سرویس بدون گروه → خروجی `appointment-service-slots` عیناً مثل قبل | ⏳ | ⭐ مهم‌ترین ردیف این تسک |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `ServiceCategory` درختی با materialized path | ⏳ | |
| ۱.۲ | `ItemGroup` با `min_select`/`max_select` | ⏳ | |
| ۱.۳ | `ServiceOption` («آیتم» مستند) با `solo_minutes`/`additional_minutes` | ⏳ | |
| ۱.۴ | `ServiceOptionRelation` — ناسازگاری متقارن، پیش‌نیاز جهت‌دار | ⏳ | |
| ۱.۵ | `assertNoCycle()` روی پیش‌نیاز — هنگام **ثبت**، نه ارزیابی | ⏳ | |
| ۱.۶ | `ServiceBranchOverride` با سه ستون تهی‌پذیر (override جزئی) | ⏳ | |
| ۱.۷ | `DurationCalculator` — اولین آیتم گروه solo، بقیه additional | ⏳ | |
| ۱.۸ | مرتب‌سازی نزولی بر `solo_minutes` + کامنت دلیل | ⏳ | قطعیت |
| ۱.۹ | `additional_minutes === null` → از `solo_minutes` (نه صفر) | ⏳ | |
| ۱.۱۰ | `ServiceSelectionValidator` — ترتیب شش‌مرحله‌ای، مالکیت محیط **اول** | ⏳ | |
| ۱.۱۱ | خطاها **همه با هم** برمی‌گردند، نه اولی | ⏳ | |
| ۱.۱۲ | `ServicePriceResolver` با override شعبه بر تعرفه | ⏳ | |
| ۱.۱۳ | `ServiceBookingCalculator` تسک ۰۰ به `DurationCalculator` وصل شد | ⏳ | ⭐ نقطهٔ اتصال — یک خط |
| ۱.۱۴ | ده endpoint | ⏳ | |
| ۱.۱۵ | `additional > solo` → ۴۲۲ | ⏳ | |
| ۱.۱۶ | سقف عمق درخت ۴ · جابه‌جایی با `UPDATE … REPLACE(path)` در تراکنش | ⏳ | |
## ۲. `POST /service-selection/validate` — عمومی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | مسیر در `security.yaml` whitelist شد | ⏳ | سایت بدون توکن صدا می‌زند |
| ۲.۲ | گارد دستی `TenantOwnershipChecker::belongsToPair()` روی همهٔ uuid ها | ⏳ | `TenantFilter` خاموش است |
| ۲.۳ | `symfony/rate-limiter` روی IP | ⏳ | enumerate کاتالوگ |
| ۲.۴ | uuid محیط دیگر → ۴۰۴ **بدون** هیچ اطلاعاتی در بدنه | ⏳ | |
## ۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | چهار جدول جدید + `service_option_relations` | ⏳ | |
| ۳.۲ | سه ستون جدید روی `service_items` (همه تهی‌پذیر یا با default) | ⏳ | |
| ۳.۳ | `idx_svc_cat_path` برای شرط دسته‌ای تسک ۰۹ | ⏳ | |
| ۳.۴ | `entity_type, entity_id` ستون اول ایندکس‌های لیست | ⏳ | |
| ۳.۵ | `service_option_relations` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۳.۶ | `TenantSchemaCoverageTest` + `TenantLookupInventoryTest` سبز | ⏳ | |
## ۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | تب «گروه‌ها و آیتم‌ها» در `ServiceDetailPage` موجود | ⏳ | صفحهٔ جدید نه، تب |
| ۴.۲ | ویرایش inline `min/max` گروه | ⏳ | |
| ۴.۳ | جدول آیتم‌ها: نام، زمان تنها، زمان اضافه، قیمت، فعال | ⏳ | |
| ۴.۴ | ناسازگاری/پیش‌نیاز با `SearchableSelect` چندانتخابی | ⏳ | |
| ۴.۵ | **پیش‌نمایش زنده مدت** با debounce ۴۰۰ms | ⏳ | ⭐ بدون آن کل تسک بی‌اثر است |
| ۴.۶ | قیمت با `PriceInput` | ⏳ | |
| ۴.۷ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۴.۸ | دارک‌مود و حالت فشرده | ⏳ | |
| ۴.۹ | RTL و موبایل | ⏳ | |
| ۴.۱۰ | فرم با React Hook Form + Zod | ⏳ | |
| ۴.۱۱ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۴.۱۲ | خطاهای اعتبارسنجی **زیر همان گروه** نمایش داده می‌شوند | ⏳ | |
## ۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `DurationCalculatorTest` — پنج حالت + **قطعیت** (جابه‌جایی ترتیب ورودی) | ⏳ | |
| ۵.۲ | `ServiceSelectionValidatorTest` — min/max/ناسازگار/پیش‌نیاز/چند خطا/۴۰۴ | ⏳ | |
| ۵.۳ | `ServicePriceResolverTest` — اولویت و override جزئی | ⏳ | |
| ۵.۴ | `ServiceCategoryTreeTest` — عمق، حذف، جابه‌جایی path | ⏳ | |
| ۵.۵ | `BackwardCompatibilityTest` | ⏳ | ⭐ |
| ۵.۶ | حلقهٔ پیش‌نیاز → ۴۲۲ | ⏳ | |
## ۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `docs/api/clinic-services.md`**جدول واژگان** عیناً از architecture | ⏳ | ⭐ بدون آن همه قاطی می‌کنند |
| ۶.۲ | endpoint های جدید | ⏳ | |
| ۶.۳ | یادآوری: `nobat724_front` قرارداد `service-selection/validate` را مصرف می‌کند | ⏳ | |
## ۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۷.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۷.۶ | تست‌های tenant سبز | ⏳ | |
| ۷.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۷.۸ | چک‌لیست UI کامل | ⏳ | |
| ۷.۹ | ⚠️ مدت نوبت‌های چندسرویسی عوض می‌شود → `nobat724_front` و `clinic-pro-tauri` دستی بررسی شدند | ⏳ | ⭐ این تسک عدد را عوض می‌کند |
| ۷.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۷.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,104 @@
# چک‌لیست — تسک ۰۵ (بخش‌های نوبت و سازندهٔ برنامه)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `SlotCalculatorService` دست‌نخورده | ⏳ | |
| ۰.۳ | سرویس بدون `SegmentTemplate` → یک بخش مجازی با منبع `type=doctor` | ⏳ | ⭐ سازگاری حالت `service` فعلی |
| ۰.۴ | هیچ جدولی برای «برنامهٔ ساخته‌شده» ساخته نشد | ⏳ | DTO درون‌حافظه؛ ذخیره کار تسک ۰۷ |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `SegmentTemplate` · `SegmentRequirement` | ⏳ | |
| ۱.۲ | DTO های `AppointmentPlan` · `PlannedSegment` · `PlannedRequirement` (immutable) | ⏳ | |
| ۱.۳ | پنج سرویس جدا: `Builder` · `Assembler` · `DurationResolver` · `RequirementResolver` | ⏳ | نه یک کلاس بزرگ |
| ۱.۴ | `build()` تابع **خالص** است — بدون write، بدون cache، بدون `time()` | ⏳ | ⭐ تسک ۰۶ رویش حساب می‌کند |
| ۱.۵ | مرحلهٔ ۷ (`policies->applyToPlan`) از روز اول در امضا، حتی no-op | ⏳ | تسک ۰۹ پرش می‌کند |
| ۱.۶ | ادغام: `count` **بیشینه**، نه جمع | ⏳ | |
| ۱.۷ | `offset_minutes` نسبی است، نه timestamp مطلق | ⏳ | تسک ۰۶ برنامه را می‌لغزاند |
| ۱.۸ | `occupancyStartOffset`/`occupancyEndOffset` جدا از offset نمایشی | ⏳ | `setup/cleanup` |
| ۱.۹ | قید جنسیت بدون داده → ۴۲۲، **نه نادیده گرفتن** | ⏳ | ⭐ الزام جدی کلینیک زیبایی |
| ۱.۱۰ | `constraints` فهرست بسته؛ کلید ناشناخته → ۴۲۲ | ⏳ | |
| ۱.۱۱ | `NoEligibleResourceException` با پیام انسانی + `meta` | ⏳ | نقش، مهارت، شعبه |
| ۱.۱۲ | سه endpoint | ⏳ | |
| ۱.۱۳ | `patient_facing_minutes` جدا از `total_minutes` در پاسخ | ⏳ | بخش‌های بدون حضور بیمار |
| ۱.۱۴ | سقف‌ها اعمال شد: ۲۰ بخش · ۴۸۰ دقیقه · ۱۰ نیازمندی · ۲۰ آیتم | ⏳ | |
| ۱.۱۵ | `app:segment:seed-templates --preset=beauty\|dental\|physio` — dry-run | ⏳ | |
| ۱.۱۶ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `segment_templates` · `segment_requirements` | ⏳ | |
| ۲.۲ | دقیقاً یکی از `service_item_id`/`service_option_id` — قید اپلیکیشنی | ⏳ | |
| ۲.۳ | دقیقاً یکی از `fixed_minutes`/`duration_share` | ⏳ | |
| ۲.۴ | جمع `duration_share` یک سرویس = ۱۰۰ (اعتبارسنجی هنگام ذخیره) | ⏳ | |
| ۲.۵ | `required_skills` به‌صورت JSON با دلیل مکتوب | ⏳ | نه جدول واسط |
| ۲.۶ | `segment_requirements` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `ServiceSegmentsPage` به‌عنوان زیرصفحهٔ `ServiceDetailPage` | ⏳ | |
| ۳.۲ | لیست بخش‌ها با `sequence` عددی (بدون drag) | ⏳ | |
| ۳.۳ | نیازمندی‌ها: نقش با `SearchableSelect`، مهارت با چیپ، اشغال با رادیو | ⏳ | |
| ۳.۴ | هر گزینهٔ نوع اشغال توضیح فارسی دارد | ⏳ | انحصاری/اشتراکی/منفعل |
| ۳.۵ | **نوار پیش‌نمایش زمانی** با عرض متناسب مدت و آیکن منابع | ⏳ | ⭐ تنها راه فهم کاربر غیرفنی |
| ۳.۶ | خط «اپراتور واقعاً درگیر: N دقیقه» زیر نوار | ⏳ | ⭐ ارزش کار را نشان می‌دهد |
| ۳.۷ | `backTo` روی صفحه | ⏳ | |
| ۳.۸ | هیچ رنگ/شعاع hard-code — نوار پیش‌نمایش هم | ⏳ | |
| ۳.۹ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۰ | RTL و موبایل — نوار پیش‌نمایش روی موبایل اسکرول افقی داخلی دارد | ⏳ | |
| ۳.۱۱ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۳.۱۲ | `meta` خطای «هیچ منبعی» به لینک «افزودن منبع» تبدیل شد | ⏳ | بن‌بست نباشد |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `SegmentAssemblerTest` — ادغام، `count` بیشینه، غیر-mergeable | ⏳ | |
| ۴.۲ | `SegmentDurationResolverTest` — fixed ثابت، سهمی مقیاس‌پذیر، جمع = total | ⏳ | |
| ۴.۳ | `AppointmentPlanBuilderTest` — سناریوی کامل مستند (۰/۵/۳۵/۵۵، total=60) | ⏳ | |
| ۴.۴ | `AppointmentPlanBuilderTest`**قطعیت**: دو build یکسان → خروجی یکسان | ⏳ | |
| ۴.۵ | سرویس بدون الگو → یک بخش با منبع doctor | ⏳ | ⭐ |
| ۴.۶ | `RequirementResolverTest` — مهارت، هیچ کاندید، قید جنسیت، محیط دیگر | ⏳ | |
| ۴.۷ | `PlanLimitsTest` — ۲۱ بخش و ۴۸۱ دقیقه → ۴۲۲ | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/appointment-plan.md` | ⏳ | |
| ۵.۲ | جدول سه حالت اشغال و تفاوت عملی‌شان | ⏳ | |
| ۵.۳ | تفاوت `offset` نمایشی با `occupancy_offset` | ⏳ | |
| ۵.۴ | مثال کامل خروجی `preview` | ⏳ | |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | تست‌های tenant سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | `preview` هنوز مصرف نمی‌شود |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,119 @@
# چک‌لیست — تسک ۰۶ (موتور جستجوی وقت چندمنبعی)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ — حساس‌ترین تسک این فاز
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `SlotCalculatorService` **هیچ** متدی عوض نشد | ⏳ | `AvailabilityEngine` کلاس موازی |
| ۰.۳ | `GET /appointment-slots` بیت‌به‌بیت دست‌نخورده | ⏳ | |
| ۰.۴ | `GET /appointment-service-slots` دست‌نخورده | ⏳ | حالت `service` موجود |
| ۰.۵ | `GET /month-availability/{doctorUuid}` دست‌نخورده | ⏳ | |
| ۰.۶ | `LegacyBookingUnchangedTest`: همهٔ تست‌های اسلاتی و سرویسی موجود سبز | ⏳ | ⭐ |
| ۰.۷ | انتخاب موتور فقط با `match($mode)` در کنترلر — **هیچ fallback خاموشی** | ⏳ | حالت اشتباه → `ERR_WRONG_BOOKING_MODE` |
| ۰.۸ | `DEFAULT_META['booking_mode']` همچنان `slot` | ⏳ | |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `AvailabilityEngine` · `CandidateGenerator` · `ResourceAllocator` · `OccupancyIndex` | ⏳ | |
| ۱.۲ | چهار استراتژی + `ResourcePickerInterface` با tagged_iterator | ⏳ | OCP |
| ۱.۳ | `DailyWindowCache`**فقط پنجرهٔ تقویمی**، هرگز اشغال | ⏳ | ⭐ |
| ۱.۴ | `MODE_RESOURCE` + `slot_granularity` + `picker_strategy` در `meta` | ⏳ | با اعتبارسنجی |
| ۱.۵ | `POST /appointment-settings/upgrade-booking-mode` — یک‌طرفه، با شرط | ⏳ | |
| ۱.۶ | دو endpoint جستجو | ⏳ | |
| ۱.۷ | `groupKey` = `(role, skills, constraints, indexInSegment)`؛ تطبیق بین‌بخشی روی سه جزء اول | ⏳ | ⭐ تلهٔ دو نیازمندی هم‌شکل در یک بخش |
| ۱.۸ | تخصیص حریصانه (بدون backtracking) + دلیل مکتوب | ⏳ | |
| ۱.۹ | دو گذر `setup/cleanup`: بیشینهٔ کاندیدها، بعد دقیق منبع انتخابی | ⏳ | |
| ۱.۱۰ | `hasRoom` شرط `expires_at > now` روی hold | ⏳ | |
| ۱.۱۱ | `reason` در پاسخ خالی: `no_resource`/`no_calendar`/`fully_booked`/`outside_window` | ⏳ | نه ۴۰۴، نه پیام واحد |
| ۱.۱۲ | قلاب `policies->filterSlots` از روز اول در امضا | ⏳ | تسک ۰۹ |
| ۱.۱۳ | سقف‌ها: بازه ۹۰ روز · `limit` ۲۰۰ · گام ≥۵ · کاندید ≤۵۰ per نیازمندی | ⏳ | |
| ۱.۱۴ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. کارایی — بخشی از تسک، نه اختیاری
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | **سه** کوئری برای کل بازه؛ هیچ I/O داخل حلقه | ⏳ | ⭐ |
| ۲.۲ | هرس با «تنگ‌ترین منبع» پیاده شد | ⏳ | ~۸۰٪ کاندیدها حذف |
| ۲.۳ | `busy` مرتب + جستجوی دودویی در `OccupancyIndex` | ⏳ | |
| ۲.۴ | `app:dev:seed-availability-benchmark` | ⏳ | ۳ اتاق، ۲ اپراتور، ۳ دستگاه، ۵۰۰ نوبت |
| ۲.۵ | `AvailabilityPerformanceTest`: **< ۵۰۰ms** | ⏳ | |
| ۲.۶ | `AvailabilityPerformanceTest`: **≤ ۵ کوئری** | ⏳ | مهم‌تر از زمان — ماشین‌مستقل |
| ۲.۷ | ابطال کش: تقویم/استثنا/ساعت شعبه/تعطیلی → `win`؛ ثبت نوبت → فقط `month` | ⏳ | |
## ۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `resource_occupancy` **در این تسک** migrate شد (تعریف در تسک ۰۷) | ⏳ | وابستگی معکوس |
| ۳.۲ | `idx_occupancy_resource_range (resource_id, start_at, end_at, status)` | ⏳ | `resource_id` اول — نه tenant |
| ۳.۳ | ترتیب ستون‌های ایندکس در migration **دستی** نوشته شد | ⏳ | `diff` گاهی جابه‌جا می‌کند |
| ۳.۴ | `setMeta` کلیدهای جدید را با اعتبارسنجی می‌پذیرد | ⏳ | مقدار نامعتبر → مقدار فعلی |
## ۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `AppointmentSettingsPage` انتخاب حالت `resource` + گام + استراتژی | ⏳ | |
| ۴.۲ | چک‌لیست پیش از ارتقا با علامت ✓/✗ هر شرط | ⏳ | ⭐ بدون آن ارتقای اشتباه |
| ۴.۳ | تیک «می‌دانم برگشت‌ناپذیر است» اجباری | ⏳ | |
| ۴.۴ | جدول وقت‌ها با ستون «منابع پیشنهادی» و `SearchableSelect` per منبع | ⏳ | پنل |
| ۴.۵ | عوض کردن یک منبع → اعتبارسنجی **همان زمان**، نه کل لیست | ⏳ | |
| ۴.۶ | `reason` خالی‌بودن با پیام فارسی + دکمهٔ پیشنهادی | ⏳ | چهار حالت |
| ۴.۷ | `assignment` به بیمار نمایش داده **نمی‌شود** | ⏳ | فقط پنل |
| ۴.۸ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۴.۹ | دارک‌مود و حالت فشرده | ⏳ | |
| ۴.۱۰ | RTL و موبایل | ⏳ | |
| ۴.۱۱ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۴.۱۲ | `ScheduleSection.tsx` موجود توسعه یافت، کامپوننت موازی ساخته نشد | ⏳ | |
## ۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `OccupancyIndexTest` — capacity، بازهٔ مماس، shared/exclusive | ⏳ | واحد |
| ۵.۲ | `CandidateGeneratorTest` — هرس، گذشته، برنامهٔ جا‌نشو | ⏳ | |
| ۵.۳ | `ResourceAllocatorTest` — منبع مشترک یکی؛ دو هم‌شکل در یک بخش دو منبع | ⏳ | |
| ۵.۴ | `CapacityReleaseTest`**آزادسازی ظرفیت** | ⏳ | ⭐⭐ بدون این تسک تأیید نمی‌شود |
| ۵.۵ | `StrategyTest` — سه استراتژی | ⏳ | |
| ۵.۶ | `BookingModeGuardTest` — حالت اشتباه دو طرفه ۴۲۲ + ارتقا با نوبت فعال | ⏳ | |
| ۵.۷ | `AvailabilityPerformanceTest` | ⏳ | |
| ۵.۸ | `LegacyBookingUnchangedTest` | ⏳ | ⭐ |
## ۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `docs/api/appointment-availability.md` | ⏳ | |
| ۶.۲ | جدول استراتژی‌ها | ⏳ | |
| ۶.۳ | محدودیت تخصیص حریصانه مکتوب | ⏳ | |
| ۶.۴ | ماتریس «کدام endpoint در کدام حالت» | ⏳ | |
| ۶.۵ | `docs/architecture/booking-modes.md` (تسک ۰۰) حالت سوم را گرفت | ⏳ | |
| ۶.۶ | در `docs/api/appointment.md` برجسته: کلاینت‌ها پس از ارتقا باید مسیر جدید بزنند | ⏳ | ⭐ |
## ۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۷.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۷.۶ | تست‌های tenant سبز | ⏳ | |
| ۷.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۷.۸ | چک‌لیست UI کامل | ⏳ | |
| ۷.۹ | ⚠️ `nobat724_front` و `clinic-pro-tauri`: تا کلینیک ارتقا نداده، تغییری لازم نیست — تأیید شد | ⏳ | ⭐ |
| ۷.۱۰ | تسک frontend حالت `resource` برای سایت ثبت شد (خارج از این فاز) | ⏳ | |
| ۷.۱۱ | commit، سپس `graphify update .` | ⏳ | |
| ۷.۱۲ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,122 @@
# چک‌لیست — تسک ۰۷ (رزرو موقت و ثبت نهایی چندمنبعی)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `active_slot_key` و `refreshActiveSlotKey()` دست‌نخورده و **فعال** | ⏳ | دو تور ایمنی موازی |
| ۰.۳ | `slot_start`/`slot_end` باقی ماندند | ⏳ | چهار مصرف‌کننده رویشان کوئری می‌زنند |
| ۰.۴ | `is_reserve` دست‌نخورده — رزرو هیچ ردیف اشغالی نمی‌سازد | ⏳ | |
| ۰.۵ | `POST /api/v1/appointment` قدیمی بیت‌به‌بیت کار می‌کند | ⏳ | `LegacyBookingUnchangedTest` |
| ۰.۶ | `PAYMENT_TTL` و رفتار انقضای موجود حفظ شد | ⏳ | |
## ۱. تضمین همزمانی — قلب تسک
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `resource_occupancy_slot` با `UNIQUE(resource_id, bucket, unit_index)` | ⏳ | ⭐ کل تضمین اینجاست |
| ۱.۲ | `BUCKET_SECONDS = 300` ثابت + کامنت هشدار تغییرش | ⏳ | |
| ۱.۳ | سطل‌ها با `intdiv($end - 1, 300)` — نه بدون `-1` | ⏳ | ⭐ وگرنه نوبت مجاور رد می‌شود |
| ۱.۴ | `unit_index` با **INSERT پشت‌سرهم**، نه `SELECT` قبلش | ⏳ | ⭐ پنجرهٔ رقابت |
| ۱.۵ | ردیف‌ها مرتب بر `(resource_id, bucket, unit_index)` درج می‌شوند | ⏳ | ⭐ جلوگیری از deadlock |
| ۱.۶ | `SlotTakenException` موجود بازاستفاده شد | ⏳ | |
| ۱.۷ | محدودیت گرانولاریتی ۵ دقیقه در مستندات صریح | ⏳ | |
## ۲. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `ResourceOccupancy` · `AppointmentSegment` | ⏳ | |
| ۲.۲ | `OccupancyWriter`**تنها** نویسندهٔ `resource_occupancy` | ⏳ | |
| ۲.۳ | `HoldService` · `BookingService` · `RescheduleService` | ⏳ | |
| ۲.۴ | **یک ردیف per (بخش × منبع)** — نه per نوبت | ⏳ | ⭐ آزادسازی ظرفیت |
| ۲.۵ | برنامه در `hold` **دوباره ساخته می‌شود**؛ `assignment` کلاینت فقط اعتبارسنجی می‌شود | ⏳ | ⭐ سه نشتی ثبت‌شده از همین شکل بودند |
| ۲.۶ | منبع باید **کاندید همان نیازمندی** باشد، نه فقط هم‌محیط | ⏳ | |
| ۲.۷ | `confirm` هفت مرحله در **یک** تراکنش | ⏳ | |
| ۲.۸ | `confirm` idempotent — دوباره روی همان hold خطا نمی‌دهد | ⏳ | |
| ۲.۹ | رویداد **بعد از** commit (`DispatchAfterCurrentBusStamp`) با uuid در payload | ⏳ | ⭐ تسک ۱۲ رویش حساب می‌کند |
| ۲.۱۰ | `reschedule`: اول hold جدید، بعد آزادسازی قدیم | ⏳ | ⭐ ترتیب |
| ۲.۱۱ | لغو = `status='released'` + **حذف فیزیکی** ردیف‌های سطل | ⏳ | |
| ۲.۱۲ | `setup/cleanup` در بازهٔ اشغال، نه در `appointment_segments` | ⏳ | |
| ۲.۱۳ | `STATUS_RESCHEDULED` + گذارهای مجاز | ⏳ | |
| ۲.۱۴ | `ExpireAppointmentsHandler` موجود توسعه یافت | ⏳ | آزادسازی + حذف سطل |
| ۲.۱۵ | قلاب‌های تسک ۰۸ و ۰۹ در `confirm` (مراحل ۳ و ۶) | ⏳ | |
| ۲.۱۶ | چهار endpoint | ⏳ | |
| ۲.۱۷ | دو کد خطا در `ErrorCodes.php` با پیام فارسی | ⏳ | `ERR_SLOT_TAKEN` · `ERR_HOLD_EXPIRED` |
## ۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `resource_occupancy` (BIGINT id) با چهار ایندکس | ⏳ | |
| ۳.۲ | `resource_occupancy_slot` با UNIQUE | ⏳ | |
| ۳.۳ | `appointment_segments` با snapshot `name`/`segment_type` | ⏳ | قانون پنجم |
| ۳.۴ | سه ستون تهی‌پذیر روی `appointments` | ⏳ | `branch_id` · `plan_total_minutes` · `patient_facing_minutes` |
| ۳.۵ | ترتیب ستون ایندکس‌ها **دستی** در migration | ⏳ | |
| ۳.۶ | `app:occupancy:backfill --force` — idempotent، نوبت‌های بی‌منبع را گزارش می‌کند | ⏳ | ⭐ بدون آن رزرو جدید روی نوبت قدیم می‌نشیند |
| ۳.۷ | `app:occupancy:prune --older-than=90d` | ⏳ | |
| ۳.۸ | `resource_occupancy_slot` در `AGGREGATE_CHILDREN` + هرگز کوئری مستقیم | ⏳ | |
| ۳.۹ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | تایمر شمارش معکوس hold در UI رزرو | ⏳ | |
| ۴.۲ | خطای `409` با پیام «این ساعت همین لحظه رزرو شد» + **لیست جایگزین خودکار** | ⏳ | ⭐ مستند بند ۱۷ |
| ۴.۳ | خطای `reschedule` شامل «نوبت فعلی تغییری نکرد» | ⏳ | ⭐ |
| ۴.۴ | مسدودسازی موردی منبع از صفحهٔ منابع | ⏳ | |
| ۴.۵ | تفکیک «مسدودسازی موردی» (occupancy) از «بلندمدت» (exception) در UI روشن است | ⏳ | دو راه یک کار گیج‌کننده است |
| ۴.۶ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۴.۷ | دارک‌مود و حالت فشرده | ⏳ | |
| ۴.۸ | RTL و موبایل | ⏳ | |
| ۴.۹ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۴.۱۰ | `AppointmentDetailPage` بخش بخش‌های نوبت (فقط حالت `resource`) | ⏳ | |
## ۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `ConcurrentHoldTest`**دو اتصال واقعی**، دقیقاً یکی موفق | ⏳ | ⭐⭐ mock قبول نیست |
| ۵.۲ | `OccupancyWriterTest` — بازهٔ مماس، capacity، ترتیب INSERT | ⏳ | |
| ۵.۳ | `HoldLifecycleTest` — hold/انقضا/آزادسازی زودهنگام | ⏳ | |
| ۵.۴ | `BookingConfirmTest` — hold دیگری ۴۰۴، منقضی ۴۰۹، idempotent | ⏳ | |
| ۵.۵ | `CapacityReleaseIntegrationTest` | ⏳ | ⭐⭐ اپراتور در بازهٔ انتظار ردیف ندارد |
| ۵.۶ | `RescheduleTest` — شکست hold جدید → نوبت قدیم سالم | ⏳ | |
| ۵.۷ | `OccupancyBackfillTest` — idempotent | ⏳ | |
| ۵.۸ | `LegacyBookingUnchangedTest` | ⏳ | ⭐ |
| ۵.۹ | `BookingTenantTest` موجود سبز | ⏳ | |
## ۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `docs/api/appointment-booking.md` | ⏳ | |
| ۶.۲ | گرانولاریتی ۵ دقیقه و محدودیتش | ⏳ | |
| ۶.۳ | قرارداد `hold_uuid` و TTL | ⏳ | |
| ۶.۴ | تفکیک مسدودسازی موردی/بلندمدت | ⏳ | |
| ۶.۵ | `docs/architecture/booking-concurrency.md` — سطل زمانی + دلیل رد دو گزینهٔ دیگر | ⏳ | ⭐ شش ماه بعد زیر سؤال می‌رود |
## ۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۷.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۷.۶ | تست‌های tenant سبز | ⏳ | |
| ۷.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۷.۸ | چک‌لیست UI کامل | ⏳ | |
| ۷.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | `slot_start/slot_end` سالم است؟ |
| ۷.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۷.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,108 @@
# چک‌لیست — تسک ۰۸ (لیست قیمت بازه‌دار و snapshot فاکتور)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `Tariff` دست‌نخورده — سطر ۴ زنجیرهٔ `PriceResolver` | ⏳ | |
| ۰.۳ | `AppointmentInsuranceService` و `TenantServiceCoverage` بازنویسی **نشدند** | ⏳ | قاعدهٔ «اول بگرد» |
| ۰.۴ | `DiscountRule`/`DiscountEngine` دست‌نخورده | ⏳ | |
| ۰.۵ | `Invoice`/`InvoiceItem` دست‌نخورده و حذف نشدند | ⏳ | کار متفاوتی می‌کنند |
| ۰.۶ | ستون‌های موجود نوبت استفاده شدند، ستون جدید مالی روی `appointments` اضافه نشد | ⏳ | |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `PriceList` · `PriceListItem` · `PriceSnapshot` · `PriceSnapshotLine` · `DepositPolicy` | ⏳ | |
| ۱.۲ | `PricingEngine` — هفت مرحله، هر کدام سرویس مستقل | ⏳ | |
| ۱.۳ | مراحل ۳ و ۴ از روز اول در زنجیره، حتی no-op | ⏳ | تسک ۰۹ و ۱۱ |
| ۱.۴ | `PriceResolver` — ترتیب پنج‌گانه، هرگز صفر یا خطا | ⏳ | |
| ۱.۵ | تاریخ مبنا = `slot_start` (تاریخ رزرو)، نه `time()` | ⏳ | ⭐ دو تفسیر دارد |
| ۱.۶ | همهٔ محاسبات با `intdiv`، هیچ float در مسیر پول | ⏳ | |
| ۱.۷ | تخفیف **پشت‌سرهم**، نه جمع درصدها | ⏳ | ۴۰ سپس ۱۰ = ۴۶ |
| ۱.۸ | سقف جمع تخفیف اعمال شد | ⏳ | |
| ۱.۹ | `final = max(0, …)` + ردیف `adjustment` هنگام فعال شدن سقف | ⏳ | |
| ۱.۱۰ | `appliedPolicyIds` از روز اول ثبت می‌شود | ⏳ | |
| ۱.۱۱ | `PriceSnapshotLine.label` و `source_id` بدون FK (کپی متنی) | ⏳ | قانون پنجم |
| ۱.۱۲ | `DepositCalculator` روی ستون‌های موجود نوبت می‌نویسد | ⏳ | |
| ۱.۱۳ | `activate` تداخل بازه را می‌سنجد؛ لیست شعبه با محیط تداخل ندارد | ⏳ | |
| ۱.۱۴ | هفت endpoint | ⏳ | |
| ۱.۱۵ | قلاب مرحلهٔ ۶ `BookingService::confirm` وصل شد | ⏳ | |
| ۱.۱۶ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | پنج جدول | ⏳ | |
| ۲.۲ | `final_rials` و `amount_rials` از نوع **BIGINT** | ⏳ | ⭐ پکیج بزرگ از سقف INT عبور می‌کند |
| ۲.۳ | بقیهٔ `price_rials` ها `INT` ماندند | ⏳ | قیمت واحد عبور نمی‌کند |
| ۲.۴ | `UNIQUE(appointment_id)` روی snapshot | ⏳ | |
| ۲.۵ | `price_list_items` و `price_snapshot_lines` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۶ | `app:pricing:backfill-snapshots --force` — idempotent | ⏳ | نوبت‌های موجود فاکتور خالی نداشته باشند |
| ۲.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `PriceListsPage` · `PriceListFormPage` | ⏳ | |
| ۳.۲ | وضعیت شمسی: پیش‌نویس/فعال/منقضی با `StatusBadge` | ⏳ | |
| ۳.۳ | بازهٔ تاریخ با `PersianDatePicker` | ⏳ | |
| ۳.۴ | قیمت‌ها با `PriceInput` | ⏳ | |
| ۳.۵ | شعبه با `SearchableSelect` | ⏳ | |
| ۳.۶ | **«کپی از لیست قیمت قبلی»** | ⏳ | ⭐ با ۲۰۰ سرویس بدون آن لیست جدید ساخته نمی‌شود |
| ۳.۷ | کارت «فاکتور» در `AppointmentDetailPage` با ردیف‌های snapshot | ⏳ | |
| ۳.۸ | متن «قیمت بر اساس تاریخ نوبت محاسبه شده است» | ⏳ | |
| ۳.۹ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۳.۱۰ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۱ | RTL و موبایل | ⏳ | |
| ۳.۱۲ | مبالغ با `formatRial` · تاریخ‌ها با `formatDate` | ⏳ | |
| ۳.۱۳ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
| ۳.۱۴ | همهٔ رشته‌ها فارسی | ⏳ | |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `PriceResolverTest` — ترتیب پنج‌گانه + fallback | ⏳ | |
| ۴.۲ | `PricingEngineTest` — تخفیف پشت‌سرهم، سقف، منفی → صفر | ⏳ | |
| ۴.۳ | **invariant**: جمع ردیف‌ها = مبلغ نهایی، در همهٔ سناریوها | ⏳ | ⭐ |
| ۴.۴ | `PriceSnapshotImmutabilityTest` — قانون پنجم | ⏳ | ⭐ تغییر قیمت و حذف قانون |
| ۴.۵ | `PriceListActivationTest` — تداخل هم‌سطح ۴۲۲، شعبه/محیط بی‌تداخل | ⏳ | |
| ۴.۶ | `DepositCalculatorTest` — درصدی با min/max، اولویت سرویس | ⏳ | |
| ۴.۷ | `QuoteTenantTest` — سرویس محیط دیگر ۴۰۴ | ⏳ | |
| ۴.۸ | نوبت بدون سرویس (حالت `slot`) → snapshot با `visit_price_rials` | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/pricing.md` | ⏳ | |
| ۵.۲ | تصمیم «تاریخ رزرو، نه تاریخ ثبت» صریح | ⏳ | |
| ۵.۳ | `docs/architecture/insurance-billing-system.md` جدول `PriceSnapshot` vs `Invoice` | ⏳ | ⭐ وگرنه یکی حذف می‌شود |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | تست‌های tenant سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | ⚠️ مبلغ نمایشی رزرو ممکن است عوض شود → دو کلاینت دستی بررسی شدند | ⏳ | |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,120 @@
# چک‌لیست — تسک ۰۹ (موتور قوانین شش‌دسته‌ای)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | **هیچ قانونی روی حالت `slot` اعمال نمی‌شود** | ⏳ | ⭐ حتی اگر منطقی به نظر برسد |
| ۰.۳ | `DiscountRule` مهاجرت نکرد و دست‌نخورده ماند | ⏳ | |
| ۰.۴ | `NoPolicyRegressionTest`: بدون هیچ قانون، خروجی‌ها بیت‌به‌بیت مثل تسک ۰۸ | ⏳ | ⭐ |
| ۰.۵ | کد دلخواه در قانون **ممنوع** — فقط فهرست بسته | ⏳ | مستند بند ۸ |
| ۰.۶ | تودرتویی شرط ممنوع — فقط `all`/`any` یک‌سطحی | ⏳ | |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `Policy` · `PolicyVersionLog` | ⏳ | |
| ۱.۲ | `active = false` پیش‌فرض | ⏳ | تسک ۱۰ آزمایش را اجبار می‌کند |
| ۱.۳ | `FieldRegistry` — سه مسئولیت روی یک آرایه (schema/extract/assert) | ⏳ | ⭐ فیلد نمایشیِ بی‌ارزیابی ممکن نشود |
| ۱.۴ | `OperatorRegistry` با یازده عملگر شامل `days_since` | ⏳ | |
| ۱.۵ | `EffectRegistry` — اثر خارج از دسته → ۴۲۲ | ⏳ | |
| ۱.۶ | `Combiner` — جدول ترکیب مستند بند ۸، خالص و بدون I/O | ⏳ | |
| ۱.۷ | `PolicyResolver` — اولویت → اختصاصی‌بودن → قدمت | ⏳ | |
| ۱.۸ | `specificity` هنگام **ذخیره** محاسبه می‌شود، نه اجرا | ⏳ | |
| ۱.۹ | شش موتور جدا، هر کدام یک کلاس | ⏳ | نه یک `PolicyEngine` بزرگ |
| ۱.۱۰ | `evaluateIsolated()` روی هر شش موتور | ⏳ | ⭐ تسک ۱۰ به آن نیاز دارد — اینجا اضافه شود |
| ۱.۱۱ | `SpacingPolicyEngine::forbiddenRanges()` — کوئری، **نه حلقه per slot** | ⏳ | ⭐ |
| ۱.۱۲ | بازهٔ ممنوعه **پیش از** تولید کاندید به `CandidateGenerator` می‌رود | ⏳ | نه فیلتر بعدی |
| ۱.۱۳ | `combinable=false` → short-circuit؛ `deny` همیشه short-circuit | ⏳ | |
| ۱.۱۴ | فیلد بی‌مقدار → `false` **با لاگ**، نه سکوت | ⏳ | ⭐ قانون خاموش بی‌صدا |
| ۱.۱۵ | `PATCH` محتوای قانون وجود ندارد؛ فقط `name` و `active` | ⏳ | نسخه‌بندی |
| ۱.۱۶ | `policy_version_log` snapshot **کامل** نگه می‌دارد، نه diff | ⏳ | |
| ۱.۱۷ | `valid_from` گذشته در نسخهٔ جدید → ۴۲۲ | ⏳ | قانون پنجم |
| ۱.۱۸ | شش endpoint شامل `GET /policy-schema` | ⏳ | |
| ۱.۱۹ | `PricingPolicyEngine` هر دو منبع (`DiscountRule` + `Policy`) را ترکیب می‌کند | ⏳ | |
| ۱.۲۰ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. پر کردن قلاب‌های تسک‌های قبل
| # | قلاب | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | تسک ۰۴ — `ServiceSelectionValidator``SelectionPolicyEngine` | ⏳ | |
| ۲.۲ | تسک ۰۵ — `AppointmentPlanBuilder` مرحلهٔ ۷`Resource` + `Timing` | ⏳ | |
| ۲.۳ | تسک ۰۶ — `AvailabilityEngine` مرحلهٔ ۶ → `Spacing` | ⏳ | |
| ۲.۴ | تسک ۰۷ — `BookingService::confirm` مرحلهٔ ۳ → `Eligibility` | ⏳ | |
| ۲.۵ | تسک ۰۸ — `PricingEngine` مرحلهٔ ۳ → `Pricing` | ⏳ | |
| ۲.۶ | **هیچ امضایی عوض نشد** | ⏳ | ⭐ دلیل گذاشتن قلاب‌ها از روز اول |
## ۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `policies` + `policy_version_log` | ⏳ | |
| ۳.۲ | `idx_policies_lookup (entity_type, entity_id, category, active, valid_from)` | ⏳ | |
| ۳.۳ | `appointments.applied_policies` (JSON تهی‌پذیر) | ⏳ | |
| ۳.۴ | قرارداد `applied_policy_ids` با `{id, version, name}` | ⏳ | `name` کپی متنی |
| ۳.۵ | `policy_version_log` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۳.۶ | `app:policy:seed-examples` — پنج نمونه، همه `active=false` | ⏳ | |
| ۳.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۴. کارایی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `AvailabilityPerformanceTest` **با قوانین فعال** سبز است | ⏳ | ⭐⭐ اگر قرمز شد، `spacing` حلقه می‌زند |
| ۴.۲ | `SpacingPolicyEngine` تعداد کوئری ثابت دارد، مستقل از تعداد اسلات | ⏳ | |
## ۵. UI
این تسک صفحه نمی‌سازد (تسک ۱۰ می‌سازد). فقط:
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | پیام‌های خطای `deny` فارسی و قابل فهم بیمار | ⏳ | نه نام قانون خام |
| ۵.۲ | خطای `add_requirement` بدون منبع شامل **نام قانون** | ⏳ | «قانون X جراح می‌خواهد ولی…» |
## ۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `ConditionEvaluatorTest` — همهٔ عملگرها × نوع‌ها، `all`/`any`، فیلد ناموجود | ⏳ | واحد |
| ۶.۲ | `CombinerTest` — شش قاعدهٔ جدول مستند | ⏳ | واحد |
| ۶.۳ | `PolicyResolverTest` — سه سناریوی حل تناقض + short-circuit | ⏳ | |
| ۶.۴ | `SpacingPolicyEngineTest` — بازهٔ ممنوعه + تعداد کوئری ثابت | ⏳ | |
| ۶.۵ | `PolicyVersioningTest` — قانون پنجم | ⏳ | ⭐ |
| ۶.۶ | `PolicyIntegrationTest` — چهار دسته end-to-end | ⏳ | |
| ۶.۷ | `PolicySchemaTest` — هر فیلد schema قابل extract است | ⏳ | ⭐ |
| ۶.۸ | `NoPolicyRegressionTest` | ⏳ | ⭐ |
## ۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `docs/api/policy.md` با فهرست کامل فیلد/عملگر/اثر | ⏳ | |
| ۷.۲ | قاعدهٔ «`DiscountRule` یا `Policy`؟» صریح | ⏳ | ⭐ |
| ۷.۳ | `docs/architecture/policy-engine.md` — حل تناقض، ترکیب، دلیل ممنوعیت کد دلخواه، دلیل عدم مهاجرت | ⏳ | |
## ۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۸.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۸.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۸.۴ | `AvailabilityPerformanceTest` با قوانین فعال سبز | ⏳ | |
| ۸.۵ | `phpstan` بدون خطای جدید | ⏳ | |
| ۸.۶ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۸.۷ | تست‌های tenant سبز | ⏳ | |
| ۸.۸ | `docs/api/*` به‌روز | ⏳ | |
| ۸.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | پیام‌های `deny` در سایت درست نمایش داده می‌شوند؟ |
| ۸.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۸.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,102 @@
# چک‌لیست — تسک ۱۰ (فرم ساخت قانون و محیط آزمایش)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | شبیه‌سازی **هیچ ردیفی** نمی‌نویسد (جز `policy_simulation_runs`) | ⏳ | ⭐⭐ با شمارش ردیف اثبات شود |
| ۰.۳ | نوبت‌های واقعی بیماران در شبیه‌سازی تغییر نکردند | ⏳ | |
## ۱. بک‌اند — شبیه‌ساز
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `PolicySimulator` · `SimulationSampler` · `PolicySimulationRun` | ⏳ | |
| ۱.۲ | سه لایهٔ تضمین: DTO · تراکنش با rollback در `finally` · تست شمارش | ⏳ | ⭐ |
| ۱.۳ | `$this->em->clear()` بعد از rollback | ⏳ | ⭐ وگرنه entity کثیف در identity map |
| ۱.۴ | `PolicySimulationRun` **بعد از** rollback و در تراکنش جدا ثبت می‌شود | ⏳ | |
| ۱.۵ | `evaluateIsolated()` — فقط همان قانون، بدون `Resolver` و `Combiner` | ⏳ | |
| ۱.۶ | فیلتر شعبه و سرویس از **خودِ شرط قانون** استخراج می‌شود | ⏳ | وگرنه «۰٪ تحت تأثیر» گمراه‌کننده |
| ۱.۷ | سقف نمونه ۵۰؛ درخواست بیشتر → ۴۲۲ | ⏳ | |
| ۱.۸ | `PolicyTemplateRegistry` با پنج الگو | ⏳ | |
| ۱.۹ | `activate` شرط `simulate` **همان نسخه** را می‌سنجد | ⏳ | ⭐ نسخهٔ ۱ اجازهٔ نسخهٔ ۲ نمی‌دهد |
| ۱.۱۰ | محیط بدون نوبت → `simulate` خالی موفق، `activate` مجاز | ⏳ | ⭐ کلینیک جدید قفل نشود |
| ۱.۱۱ | چهار سطح شدت با آستانه‌های مستند | ⏳ | |
| ۱.۱۲ | دو endpoint | ⏳ | |
| ۱.۱۳ | `app:policy:prune-simulations` — آخرین اجرا per (policy, version) هرگز حذف نمی‌شود | ⏳ | `activate` به آن وابسته است |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `policy_simulation_runs` با جفت tenant | ⏳ | |
| ۲.۲ | `report` سقف ۵۰ ردیف | ⏳ | |
| ۲.۳ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `PoliciesPage` · `PolicyFormPage` · `PolicySimulationPage` | ⏳ | |
| ۳.۲ | `PolicyConditionBuilder` **از `GET /policy-schema`** ساخته می‌شود | ⏳ | ⭐ هیچ فیلد hard-code |
| ۳.۳ | عملگرها per فیلد **فیلتر** می‌شوند، نه همه | ⏳ | ⭐ وگرنه ۴۲۲ بی‌توضیح |
| ۳.۴ | نوع ورودی مقدار از `schema.fields[f].type` | ⏳ | |
| ۳.۵ | همهٔ select ها `SearchableSelect`؛ هیچ `<select>` بومی | ⏳ | |
| ۳.۶ | انتخاب الگو → فرم کوتاه مقدارها (مسیر ۹۰٪ کاربران) | ⏳ | |
| ۳.۷ | ستون «وضعیت فعلی → با این قانون» در گزارش | ⏳ | ⭐ تنها چیزی که کاربر غیرفنی می‌فهمد |
| ۳.۸ | درصد تحت تأثیر + سطح شدت با رنگ توکن‌محور | ⏳ | |
| ۳.۹ | شدت `none` هم هشدار می‌دهد، با متن دو‌حالتی | ⏳ | |
| ۳.۱۰ | شدت `high` → متن «مطمئنید؟» روی دکمهٔ فعال‌سازی | ⏳ | |
| ۳.۱۱ | `ConfirmDialog` موجود برای فعال‌سازی | ⏳ | نه مودال دست‌ساز |
| ۳.۱۲ | `DataTable` برای لیست قوانین با فیلتر دسته/وضعیت در URL | ⏳ | |
| ۳.۱۳ | `backTo`/`BackButton` روی هر سه صفحه | ⏳ | |
| ۳.۱۴ | هیچ رنگ/شعاع hard-code — رنگ‌های شدت هم از توکن وضعیت | ⏳ | `--warning` `--danger` `--success` |
| ۳.۱۵ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۶ | RTL و موبایل — جدول گزارش اسکرول افقی داخلی | ⏳ | |
| ۳.۱۷ | تاریخ‌ها شمسی | ⏳ | |
| ۳.۱۸ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۳.۱۹ | نمایش تاریخچهٔ نسخه‌ها با diff | ⏳ | |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `PolicySimulatorTest` — شمارش ردیف قبل/بعد | ⏳ | ⭐⭐ |
| ۴.۲ | `PolicySimulatorTest` — استثنا در `evaluateIsolated` → rollback + clear | ⏳ | |
| ۴.۳ | `SimulationSamplerTest` — استخراج فیلتر، فقط confirmed/completed، سقف ۵۰ | ⏳ | |
| ۴.۴ | `PolicyActivationGuardTest` — چهار حالت | ⏳ | ⭐ |
| ۴.۵ | `PolicyTemplateTest` — هر الگو قانون معتبر تولید می‌کند (dataProvider) | ⏳ | ⭐ |
| ۴.۶ | `SeverityTest` — چهار آستانه | ⏳ | |
| ۴.۷ | `PolicyFormPage.test.tsx` — فیلد ساختگی از mock schema در UI ظاهر می‌شود | ⏳ | ⭐ |
| ۴.۸ | `PolicyFormPage.test.tsx` — عملگر نامعتبر برای نوع نمایش داده نمی‌شود | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/policy.md``simulate`، `policy-templates`، شرط جدید `activate` | ⏳ | |
| ۵.۲ | `docs/architecture/policy-engine.md` بخش «چرا آزمایش اجباری است» | ⏳ | ارجاع به ریسک دوم مستند |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | تست‌های tenant سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | این تسک قرارداد عمومی عوض نمی‌کند |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,114 @@
# چک‌لیست — تسک ۱۱ (پکیج و دفتر اعتبار جلسات)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | **هیچ ستون `remaining`/`used_count`/`balance` در هیچ جدولی** | ⏳ | ⭐⭐ `LedgerSchemaTest` اجبار می‌کند |
| ۰.۳ | دفتر append-only — هیچ `remove`/`update` روی ردیف‌ها | ⏳ | |
| ۰.۴ | `WalletTransaction` و منطق کیف پول دست‌نخورده | ⏳ | مفهوم متفاوت |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `Package` · `PackageService` · `PatientPackage` · `SessionCreditLedger` | ⏳ | |
| ۱.۲ | `CreditLedgerService`**تنها** نویسندهٔ دفتر | ⏳ | |
| ۱.۳ | `balance()` = `SUM(delta)`، بدون هیچ مقدار ذخیره‌شده | ⏳ | ⭐ |
| ۱.۴ | پنج `kind` تعریف شد | ⏳ | |
| ۱.۵ | `quote` **هرگز** مصرف نمی‌کند؛ فقط `confirm` | ⏳ | ⭐⭐ رفرش صفحه = از دست رفتن جلسه |
| ۱.۶ | `PriceQuote` پرچم `packageWillBeConsumed` دارد | ⏳ | |
| ۱.۷ | مانده صفر → `false`، **نه استثنا** | ⏳ | ⭐ بیمار نقدی بپردازد |
| ۱.۸ | قفل بدبینانه `PESSIMISTIC_WRITE` روی ردیف پکیج | ⏳ | با جدول مقایسه با تسک ۰۷ |
| ۱.۹ | `catch UniqueConstraintViolationException` روی `consume` → idempotent | ⏳ | |
| ۱.۱۰ | FIFO — قدیمی‌ترین پکیج منقضی‌نشده | ⏳ | LIFO یعنی پول بیمار سوخته |
| ۱.۱۱ | `valid_to` هنگام **خرید** محاسبه و ذخیره می‌شود | ⏳ | |
| ۱.۱۲ | لغو → ردیف `refund`، نه حذف `consume` | ⏳ | |
| ۱.۱۳ | `TODO` با ارجاع به تسک ۱۳ برای سیاست بازگشت اعتبار | ⏳ | نه پرچم نیم‌کاره |
| ۱.۱۴ | `adjust` فقط با نقش مدیر و با `reason` اجباری | ⏳ | |
| ۱.۱۵ | `app:package:expire` روزانه — ردیف `expiry` با `delta = -balance` | ⏳ | |
| ۱.۱۶ | قلاب مرحلهٔ ۴ `PricingEngine` وصل شد | ⏳ | |
| ۱.۱۷ | هشت endpoint | ⏳ | |
| ۱.۱۸ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | چهار جدول | ⏳ | |
| ۲.۲ | `price_rials` و `price_paid_rials` از نوع **BIGINT** | ⏳ | پکیج بزرگ |
| ۲.۳ | `UNIQUE(appointment_id, kind)` روی دفتر | ⏳ | ⭐ جلوگیری از مصرف دوباره |
| ۲.۴ | `session_count`/`price_paid_rials` روی `patient_packages` **snapshot** اند | ⏳ | قانون پنجم |
| ۲.۵ | `ON DELETE RESTRICT` روی سرویسِ پکیج فروخته‌شده | ⏳ | |
| ۲.۶ | `package_services` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۷ | دفتر **جفت tenant** دارد (نه `ENTITIES` مثل کیف پول) | ⏳ | ⭐ دلیل مکتوب |
| ۲.۸ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `PackagesPage` — تعریف با `PriceInput` و انتخاب سرویس | ⏳ | |
| ۳.۲ | کارت «پکیج‌ها» در `PatientDetailPage` با مانده و انقضا | ⏳ | |
| ۳.۳ | `PatientPackageLedgerPage` — جدول دفتر | ⏳ | |
| ۳.۴ | ستون «مانده تجمعی» **محاسبه‌شده در UI**، نه ستون DB | ⏳ | ⭐ به کاربر ثابت می‌کند عدد از کجاست |
| ۳.۵ | ستون‌های دفتر: تاریخ، نوع، تغییر، مانده تجمعی، دلیل، ثبت‌کننده، نوبت | ⏳ | |
| ۳.۶ | پیام «اعتبار پکیج تمام شده؛ این نوبت نقدی محاسبه می‌شود» | ⏳ | |
| ۳.۷ | `DataTable` با skeleton و empty state | ⏳ | |
| ۳.۸ | سرویس‌ها با `SearchableSelect` | ⏳ | |
| ۳.۹ | `backTo`/`BackButton` روی زیرصفحه‌ها | ⏳ | |
| ۳.۱۰ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۳.۱۱ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۲ | RTL و موبایل | ⏳ | |
| ۳.۱۳ | مبالغ با `formatRial` · تاریخ با `formatDate` | ⏳ | |
| ۳.۱۴ | وضعیت لیست در URL با `useUrlState` | ⏳ | |
| ۳.۱۵ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۳.۱۶ | دکمهٔ `adjust` فقط برای نقش مدیر نمایش داده می‌شود | ⏳ | `FeatureGate`/بررسی نقش |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `CreditLedgerTest``SUM(delta)` در همهٔ سناریوها، append-only | ⏳ | ⭐ |
| ۴.۲ | `LedgerSchemaTest` — هیچ ستون مانده در schema | ⏳ | ⭐⭐ |
| ۴.۳ | `QuoteDoesNotConsumeTest` — ده `quote` → مانده بی‌تغییر | ⏳ | ⭐⭐ |
| ۴.۴ | `ConcurrentConsumeTest` — مانده منفی نمی‌شود | ⏳ | |
| ۴.۵ | `IdempotentConsumeTest``confirm` دوبار → یک ردیف | ⏳ | |
| ۴.۶ | `FifoTest` | ⏳ | |
| ۴.۷ | `ExpiryTest` — ردیف `expiry` و حذف از finder | ⏳ | |
| ۴.۸ | `AdjustmentAuthTest` — منشی ۴۰۳، مدیر بی‌دلیل ۴۲۲ | ⏳ | |
| ۴.۹ | `PackageTenantTest` — پکیج محیط دیگر ۴۰۴ | ⏳ | |
| ۴.۱۰ | `PricingIntegrationTest` — ردیف `package` منفی + invariant تسک ۰۸ حفظ شد | ⏳ | ⭐ |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/package.md` | ⏳ | |
| ۵.۲ | جدول مقایسهٔ قفل بدبینانه (این تسک) با سطل زمانی (تسک ۰۷) | ⏳ | ⭐ وگرنه «یکدست‌سازی» می‌شود |
| ۵.۳ | `docs/architecture/tenancy.md` — تفاوت دفتر اعتبار با کیف پول | ⏳ | ⭐ اشتباه گرفتنشان = نشتی مالی |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `PatientWalletTenantTest` موجود سبز ماند | ⏳ | |
| ۶.۵ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۶ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۷ | تست‌های tenant سبز | ⏳ | |
| ۶.۸ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۹ | چک‌لیست UI کامل | ⏳ | |
| ۶.۱۰ | دو کلاینت دیگر بررسی شدند | ⏳ | مبلغ صفر در رزرو درست نمایش داده می‌شود؟ |
| ۶.۱۱ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۲ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | سیاست بازگشت اعتبار → تسک ۱۳ |
@@ -0,0 +1,115 @@
# چک‌لیست — تسک ۱۲ (دوره درمان)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `PatientSession` موجود دست‌نخورده | ⏳ | «مراجعهٔ انجام‌شده» ≠ «جلسهٔ دوره» |
| ۰.۳ | رویدادهای تسک ۰۷ **بعد از** commit منتشر می‌شوند | ⏳ | ⭐ وگرنه در rollback هشت پیامک اشتباه |
| ۰.۴ | `abandon` نوبت‌های `booked` را **لغو نمی‌کند** | ⏳ | عمل برگشت‌ناپذیر روی ظرفیت |
## ۱. بک‌اند
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `CourseProtocol` · `CourseProtocolStep` · `TreatmentCourse` · `CourseSession` | ⏳ | |
| ۱.۲ | `CourseStarter` · `CourseScheduler` · `CourseProgressCalculator` · `CourseSessionLinker` | ⏳ | |
| ۱.۳ | چهار فیلد فاصله و `params` **snapshot** می‌شوند | ⏳ | ⭐ قانون پنجم |
| ۱.۴ | `book-all` **همه یا هیچ** در یک تراکنش | ⏳ | ⭐⭐ رزرو نیمه‌کاره بدترین حالت |
| ۱.۵ | لنگر **متحرک** — فاصله از جلسهٔ قبلی، نه از شروع دوره | ⏳ | ⭐ |
| ۱.۶ | `findNearestInRange` — نزدیک‌ترین به **ایده‌آل**، نه اولین موجود | ⏳ | |
| ۱.۷ | لنگر پیشنهاد بعدی = آخرین جلسهٔ **`completed`**، نه `booked` | ⏳ | ⭐ |
| ۱.۸ | سقف ۹۰ روز → جلسات باقی `planned` + **پیام روشن** | ⏳ | ⭐ |
| ۱.۹ | `same_as_previous` ترجیح است نه الزام — fallback به `least_gap` | ⏳ | |
| ۱.۱۰ | `preferredResourceIds` در `PlanRequest` حمل می‌شود | ⏳ | |
| ۱.۱۱ | `SameAsPreviousPicker` تسک ۰۶ ورودی گرفت | ⏳ | |
| ۱.۱۲ | تعامل با `spacing`: `max(min)` و `min(max)`؛ بازهٔ تهی → ۴۲۲ روشن | ⏳ | سخت‌گیرانه‌تر برنده |
| ۱.۱۳ | اعتبار پکیج کمتر از جلسات → **هشدار**، نه خطا | ⏳ | |
| ۱.۱۴ | `active_course_key` با الگوی `active_slot_key` | ⏳ | ⭐ نه UNIQUE روی `status` |
| ۱.۱۵ | `CourseSessionLinker` هر دو سمت رابطه را هم‌زمان ست می‌کند | ⏳ | جای دیگری نه |
| ۱.۱۶ | جلسهٔ آخر `completed` → دوره `completed` خودکار + رویداد | ⏳ | |
| ۱.۱۷ | نُه endpoint | ⏳ | |
| ۱.۱۸ | `TenantOwnershipChecker` روی هر uuid از request | ⏳ | |
## ۲. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | چهار جدول | ⏳ | |
| ۲.۲ | `min_days <= ideal_days <= max_days` و `session_count >= 2` | ⏳ | |
| ۲.۳ | `UNIQUE(protocol_id, session_number)` و `UNIQUE(course_id, session_number)` | ⏳ | |
| ۲.۴ | `UNIQUE(appointment_id)` روی `course_sessions` | ⏳ | |
| ۲.۵ | `appointments.course_session_id` تهی‌پذیر (رابطهٔ دوطرفه، عمدی) | ⏳ | |
| ۲.۶ | `course_protocol_steps` در `AGGREGATE_CHILDREN` | ⏳ | |
| ۲.۷ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۳. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `CourseProtocolsPage` — پروتکل + جدول پارامتر جلسات | ⏳ | |
| ۳.۲ | `TreatmentCoursePage` — نوار پیشرفت، جدول جلسات، دو دکمهٔ رزرو | ⏳ | |
| ۳.۳ | کارت «دوره‌های درمان» در `PatientDetailPage` | ⏳ | |
| ۳.۴ | ستون «فاصله» عدد **واقعی** بین جلسات را نشان می‌دهد، نه ایده‌آل | ⏳ | ⭐ کلینیک نظم بیمار را می‌فهمد |
| ۳.۵ | نوار زرد هشدار عبور از حداکثر فاصله | ⏳ | |
| ۳.۶ | پیشنهاد جلسهٔ بعدی به‌صورت بنر پس از `completed` شدن جلسه | ⏳ | |
| ۳.۷ | نام منبع ترجیحی روی دکمهٔ رزرو («رزرو با اپراتور مریم») | ⏳ | |
| ۳.۸ | پس از لغو جلسهٔ وسط: پیشنهاد «بازچینی جلسات باقی‌مانده» با کلیک صریح | ⏳ | خودکار نه |
| ۳.۹ | `DataTable` برای جدول جلسات | ⏳ | |
| ۳.۱۰ | `StatusBadge` برای وضعیت جلسه و دوره | ⏳ | |
| ۳.۱۱ | تاریخ‌ها شمسی با `PersianDatePicker`/`formatDate` | ⏳ | |
| ۳.۱۲ | `backTo`/`BackButton` روی زیرصفحه‌ها | ⏳ | |
| ۳.۱۳ | هیچ رنگ/شعاع hard-code — نوار پیشرفت هم | ⏳ | |
| ۳.۱۴ | دارک‌مود و حالت فشرده | ⏳ | |
| ۳.۱۵ | RTL و موبایل | ⏳ | |
| ۳.۱۶ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۳.۱۷ | پیام سقف ۹۰ روز در UI نمایش داده می‌شود | ⏳ | ⭐ وگرنه کاربر فکر می‌کند خراب است |
## ۴. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `CourseStarterTest` — ۸ جلسه، بدون پروتکل ۴۲۲، دورهٔ دوم ۴۲۲ با `meta` | ⏳ | |
| ۴.۲ | `ProtocolSnapshotTest` | ⏳ | ⭐ |
| ۴.۳ | `CourseSchedulerTest` — لنگر متحرک، نزدیک‌ترین به ایده‌آل | ⏳ | ⭐ |
| ۴.۴ | `CourseSchedulerTest` — شکست جلسهٔ N → rollback ۱..N-1 | ⏳ | ⭐⭐ |
| ۴.۵ | `CourseSchedulerTest` — سقف ۹۰ روز + پیام | ⏳ | |
| ۴.۶ | `NextSuggestionTest` — لنگر `completed`، هشدار عبور از max | ⏳ | |
| ۴.۷ | `CourseProgressTest` | ⏳ | |
| ۴.۸ | `SameResourcePreferenceTest` — fallback بدون خطا | ⏳ | |
| ۴.۹ | `CoursePolicyInteractionTest` — سخت‌گیرانه‌تر برنده، بازهٔ تهی ۴۲۲ | ⏳ | |
| ۴.۱۰ | `CoursePackageTest` — مصرف per جلسه، هشدار نه خطا | ⏳ | |
| ۴.۱۱ | `CourseLifecycleTest` — لغو، `no_show`، تکمیل خودکار، `abandon` | ⏳ | |
## ۵. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `docs/api/course.md` | ⏳ | |
| ۵.۲ | قاعدهٔ «سخت‌گیرانه‌تر برنده» بین پروتکل و قانون | ⏳ | |
| ۵.۳ | رفتار سقف ۹۰ روز | ⏳ | |
| ۵.۴ | «`abandon` نوبت‌ها را لغو نمی‌کند» صریح | ⏳ | |
## ۶. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۶.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۶.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۶.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۶.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۶.۶ | تست‌های tenant سبز | ⏳ | |
| ۶.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۶.۸ | چک‌لیست UI کامل | ⏳ | |
| ۶.۹ | دو کلاینت دیگر بررسی شدند | ⏳ | نوبت‌های دوره در پنل بیمار درست دیده می‌شوند؟ |
| ۶.۱۰ | commit، سپس `graphify update .` | ⏳ | |
| ۶.۱۱ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,128 @@
# چک‌لیست — تسک ۱۳ (سیاست لغو، عدم حضور، لیست انتظار)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | پیش‌فرض سیاست **بدون جریمه** (`penalty_mode='none'`) | ⏳ | ⭐⭐ وگرنه لحظهٔ deploy همه مشمول جریمه |
| ۰.۳ | بیمار پرریسک **مسدود نمی‌شود** — فقط برچسب | ⏳ | ⭐ مسدودسازی = قانون `eligibility` |
| ۰.۴ | `ReserveAppointmentsPage`/`is_reserve` دست‌نخورده | ⏳ | مفهوم متفاوت از لیست انتظار |
| ۰.۵ | وضعیت‌های لغو موجود (`cancelled_by_*`, `no_show`) دست‌نخورده | ⏳ | |
## ۱. بک‌اند — لغو و جریمه
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `CancellationPolicy` · `NoShowRecord` | ⏳ | |
| ۱.۲ | `CancellationPolicyResolver` — سرویس بر محیط اولویت دارد | ⏳ | |
| ۱.۳ | `PenaltyCalculator` — شرط «لغو توسط کلینیک» **اولین خط** | ⏳ | ⭐ |
| ۱.۴ | سقف جریمه = مبلغ پرداختی (`min($penalty, $paid)`) | ⏳ | |
| ۱.۵ | نوبت نقدی → جریمه صفر + `note` | ⏳ | |
| ۱.۶ | `GET /cancellation-preview` پیش از لغو | ⏳ | ⭐ |
| ۱.۷ | `CancellationService` هفت مرحله در یک تراکنش | ⏳ | |
| ۱.۸ | جریمه در `WalletTransaction` با `setRecordedEntity()` | ⏳ | ⭐ وگرنه نشتی بین محیط‌ها |
| ۱.۹ | بازگشت اعتبار پکیج **طبق سیاست** (`credit_refundable`)، نه همیشه | ⏳ | تسک ۱۱ `TODO` را برمی‌دارد |
| ۱.۱۰ | `CourseSessionLinker::releaseSession()` صدا زده می‌شود | ⏳ | تسک ۱۲ |
| ۱.۱۱ | لغو دوباره → idempotent | ⏳ | |
| ۱.۱۲ | لغو نوبت گذشته → ۴۲۲ | ⏳ | |
## ۲. بک‌اند — عدم حضور
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `NoShowTracker` با پنجرهٔ **۱۲ ماه** | ⏳ | نه کل تاریخ |
| ۲.۲ | برچسب پرریسک از `TenantTag` موجود، نه ستون بولین جدید | ⏳ | ⭐ |
| ۲.۳ | `UNIQUE(appointment_id)` → یک رکورد per نوبت | ⏳ | |
| ۲.۴ | جدول جدا، نه ستون شمارنده روی بیمار | ⏳ | همان استدلال دفتر اعتبار |
## ۳. بک‌اند — لیست انتظار
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `WaitlistEntry` · `WaitlistService` · `WaitlistMatcher` | ⏳ | |
| ۳.۲ | **broadcast** به حداکثر ۱۰ نفر، اولین رزروکننده می‌برد | ⏳ | تصمیم مکتوب |
| ۳.۳ | متن پیامک شامل «اولین نفری که رزرو کند آن را می‌گیرد» | ⏳ | ⭐ اجباری |
| ۳.۴ | `notify_count` سقف دارد (پیشنهاد ۳) | ⏳ | جلوگیری از اسپم |
| ۳.۵ | اطلاع‌رسانی **async** روی رویداد، بیرون تراکنش لغو | ⏳ | ⭐ لغو مستقل از پیامک |
| ۳.۶ | ترتیب: `priority DESC, created_at ASC` | ⏳ | |
| ۳.۷ | `preferred_day_parts` در PHP فیلتر می‌شود | ⏳ | |
| ۳.۸ | بیمار که خودش نوبت گرفت → `converted` خودکار روی رویداد `AppointmentBooked` | ⏳ | ⭐ وگرنه پیامک اضافه می‌گیرد |
| ۳.۹ | `app:waitlist:expire` روزانه | ⏳ | |
| ۳.۱۰ | بازهٔ دلخواه > ۹۰ روز → ۴۲۲ | ⏳ | |
| ۳.۱۱ | هفت endpoint | ⏳ | |
## ۴. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | سه جدول | ⏳ | |
| ۴.۲ | `idx_waitlist_match (service_item_id, branch_id, status, desired_from, desired_to)` | ⏳ | |
| ۴.۳ | `idx_no_show_patient (patient_record_id, recorded_at)` | ⏳ | کوئری پنجرهٔ ۱۲ ماه |
| ۴.۴ | `risk_tag_uuid` بدون FK (الگوی `DiscountRule.target_tag_uuid`) | ⏳ | |
| ۴.۵ | `app:cancellation:seed-default-policy` — محافظه‌کار | ⏳ | |
| ۴.۶ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۵. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `CancellationPolicyPage` — سیاست محیط + جدول override سرویس‌ها | ⏳ | |
| ۵.۲ | `WaitlistPage` — لیست + تب «قابل تطبیق» | ⏳ | |
| ۵.۳ | دکمهٔ لغو → `ConfirmDialog` با محتوای **preview** | ⏳ | ⭐ نه لغو بعد جریمه |
| ۵.۴ | نشان «پرریسک» + شمارش عدم حضور در `PatientDetailPage` | ⏳ | |
| ۵.۵ | `ConfirmDialog` موجود استفاده شد، مودال دست‌ساز نه | ⏳ | |
| ۵.۶ | `DataTable` با فیلتر بازه/سرویس در URL | ⏳ | |
| ۵.۷ | تاریخ‌ها شمسی · مبالغ با `formatRial` | ⏳ | |
| ۵.۸ | `backTo`/`BackButton` روی زیرصفحه‌ها | ⏳ | |
| ۵.۹ | هیچ رنگ/شعاع hard-code — نشان پرریسک از `--danger-bg` | ⏳ | |
| ۵.۱۰ | دارک‌مود و حالت فشرده | ⏳ | |
| ۵.۱۱ | RTL و موبایل | ⏳ | |
| ۵.۱۲ | همهٔ رشته‌ها فارسی | ⏳ | |
| ۵.۱۳ | `ReserveAppointmentsPage` موجود دست‌نخورده ماند | ⏳ | ادغام خارج از دامنه |
## ۶. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `PenaltyCalculatorTest` — پنج حالت شامل «کلینیک همیشه صفر» | ⏳ | ⭐ |
| ۶.۲ | `CancellationServiceTest` — آزادسازی، `recorded_entity`، idempotent، گذشته ۴۲۲ | ⏳ | |
| ۶.۳ | `PolicyResolverTest` — اولویت سرویس | ⏳ | |
| ۶.۴ | `NoShowTrackerTest` — سوم برچسب، قدیمی‌تر از ۱۲ ماه نه، دوبار یک رکورد | ⏳ | |
| ۶.۵ | `NoShowTrackerTest` — بیمار پرریسک **رزرو موفق** دارد | ⏳ | ⭐ |
| ۶.۶ | `WaitlistMatcherTest` — سقف ۱۰، ترتیب، فیلتر روزبخش، `notify_count` | ⏳ | |
| ۶.۷ | `WaitlistConversionTest` | ⏳ | |
| ۶.۸ | `WaitlistAsyncTest` — شکست پیامک لغو را rollback نمی‌کند | ⏳ | ⭐ |
| ۶.۹ | `PatientWalletTenantTest` موجود سبز ماند | ⏳ | ⭐ |
| ۶.۱۰ | `CourseLifecycleTest` موجود — سیاست اعتبار اعمال شد | ⏳ | |
## ۷. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | `docs/api/cancellation.md` — preview اجباری، کلینیک بی‌جریمه | ⏳ | |
| ۷.۲ | `docs/api/waitlist.md` — تصمیم broadcast و دلیلش | ⏳ | |
## ۸. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۸.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۸.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۸.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۸.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۸.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۸.۶ | تست‌های tenant سبز | ⏳ | |
| ۸.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۸.۸ | چک‌لیست UI کامل | ⏳ | |
| ۸.۹ | ⚠️ سایت باید preview لغو را نشان دهد → `nobat724_front` بررسی و تسک ثبت شد | ⏳ | ⭐ |
| ۸.۱۰ | `clinic-pro-tauri` بررسی شد | ⏳ | |
| ۸.۱۱ | commit، سپس `graphify update .` | ⏳ | |
| ۸.۱۲ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | |
@@ -0,0 +1,119 @@
# چک‌لیست — تسک ۱۴ (رویدادهای دامنه و گزارش بهره‌وری)
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:**
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
[red-lines.md](../_shared/red-lines.md) · [ui-conventions.md](../_shared/ui-conventions.md)
---
## ۰. خط سرخ
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۰.۲ | `AppointmentEvent` موجود دست‌نخورده | ⏳ | تاریخچهٔ وضعیت ≠ رویداد دامنه |
| ۰.۳ | پیامک‌های موجود (`Sms` domain) نشکستند | ⏳ | |
| ۰.۴ | گزارش با داده حدسی ساخته **نشد** | ⏳ | ⭐ بند ۱.۹ |
## ۱. بک‌اند — رویدادها
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۱.۱ | `DomainEvent` پایه + `DomainEventPublisher` + `DomainEventLog` | ⏳ | |
| ۱.۲ | payload **فقط uuid و اسکالر** — هیچ entity | ⏳ | ⭐ |
| ۱.۳ | هر رویداد `entityType`/`entityId` دارد | ⏳ | وگرنه پیامک محیط اشتباه |
| ۱.۴ | الگوی **outbox**: `record()` داخل تراکنش کاری، فقط persist | ⏳ | ⭐ |
| ۱.۵ | `PublishDomainEventHandler` + `scheduler` هر ۱۰ ثانیه | ⏳ | |
| ۱.۶ | `attempts < 5`؛ ردیف شکست‌خورده **حذف نمی‌شود** | ⏳ | |
| ۱.۷ | همهٔ `dispatch` های تسک‌های ۰۷ تا ۱۳ به `record()` تغییر کردند | ⏳ | ⭐ |
| ۱.۸ | چهارده رویداد بند ۱۶ مستند ثبت شدند | ⏳ | |
| ۱.۹ | idempotency در **مصرف‌کننده**، با `domain_events.uuid` | ⏳ | at-least-once |
| ۱.۱۰ | worker با loop-wrap برای Coolify | ⏳ | کانتینر خارج نشود |
| ۱.۱۱ | `app:events:prune --older-than=180d` | ⏳ | |
| ۱.۱۲ | `GET /domain-events` فقط `ROLE_ADMIN` | ⏳ | |
## ۲. بک‌اند — گزارش‌ها
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۲.۱ | `ResourceUtilizationReporter` با چهار عدد | ⏳ | |
| ۲.۲ | `available_minutes` **× `capacity`** منبع | ⏳ | ⭐ اتاق سه‌تخته سه برابر |
| ۲.۳ | `passive` در `occupied` هست، در `active` نه | ⏳ | |
| ۲.۴ | `setup/cleanup` در `occupied` هست | ⏳ | |
| ۲.۵ | `released` شمرده نمی‌شود (`status='booked'` فقط) | ⏳ | |
| ۲.۶ | `available = 0``utilization = null`، **نه صفر** | ⏳ | ⭐ معنای متفاوت |
| ۲.۷ | مرز بازه: `start_at >= from AND start_at < to` | ⏳ | نه `end_at <= to` |
| ۲.۸ | کوئری تجمعی با `GROUP BY`، بدون پیمایش | ⏳ | |
| ۲.۹ | **پیش از پیاده‌سازی** `plan-accuracy`: وجود `patient_sessions.started_at/ended_at` تأیید شد | ⏳ | ⭐ اگر نبود → تسک جدا، نه داده حدسی |
| ۲.۱۰ | `PlanAccuracyReporter` با آستانه‌های شدت | ⏳ | |
| ۲.۱۱ | انحراف **منفی** بزرگ هم `high` است | ⏳ | نصف ظرفیت هدر می‌رود |
| ۲.۱۲ | حداقل نمونه ۱۰، وگرنه `insufficient_data` | ⏳ | |
| ۲.۱۳ | بازه > ۹۰ روز → ۴۲۲ | ⏳ | |
| ۲.۱۴ | سه endpoint | ⏳ | |
## ۳. دیتابیس
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۳.۱ | `domain_events` (BIGINT id) با سه ایندکس | ⏳ | |
| ۳.۲ | `idx_de_pending (published_at, occurred_at)` | ⏳ | کوئری worker |
| ۳.۳ | هیچ جدول دیگری تغییر نکرد | ⏳ | |
| ۳.۴ | `TenantSchemaCoverageTest` سبز | ⏳ | |
## ۴. UI
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۴.۱ | `ResourceUtilizationPage` — جدول + نمودار `Recharts` | ⏳ | کتابخانهٔ موجود |
| ۴.۲ | `PlanAccuracyPage` — جدول انحراف با شدت | ⏳ | |
| ۴.۳ | ردیف‌های `active_ratio < 0.3` نشان هشدار دارند | ⏳ | |
| ۴.۴ | **tooltip توضیح `active_ratio` در خودِ UI** | ⏳ | ⭐ نه فقط در مستندات |
| ۴.۵ | `utilization = null``—` با tooltip «تقویم تعریف نشده» + لینک تنظیم | ⏳ | |
| ۴.۶ | لینک «ویرایش بخش‌های این سرویس» از `PlanAccuracyPage` | ⏳ | ⭐ گزارشی که راه اصلاح ندهد خوانده نمی‌شود |
| ۴.۷ | بازهٔ زمانی با `PersianDatePicker` | ⏳ | |
| ۴.۸ | وضعیت (بازه، فیلتر) در URL با `useUrlState` | ⏳ | |
| ۴.۹ | `DataTable` با skeleton و empty state | ⏳ | |
| ۴.۱۰ | رنگ نمودار از توکن‌های `--stat-*`، نه پالت پیش‌فرض Recharts | ⏳ | ⭐ |
| ۴.۱۱ | هیچ رنگ/شعاع hard-code | ⏳ | |
| ۴.۱۲ | دارک‌مود — نمودار هم در دارک خوانا است | ⏳ | ⭐ محور و legend |
| ۴.۱۳ | حالت فشرده | ⏳ | |
| ۴.۱۴ | RTL و موبایل — جدول و نمودار اسکرول افقی داخلی | ⏳ | |
| ۴.۱۵ | همهٔ رشته‌ها فارسی · اعداد با `formatNumber` | ⏳ | |
| ۴.۱۶ | `backTo` روی صفحات گزارش | ⏳ | |
## ۵. تست
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۵.۱ | `OutboxTest` — record داخل تراکنش، rollback، انتشار، شکست، سقف تلاش | ⏳ | ⭐ |
| ۵.۲ | `EventPayloadTest` — reflection روی همهٔ زیرکلاس‌ها: فقط اسکالر | ⏳ | |
| ۵.۳ | `ResourceUtilizationTest` — شش سنجهٔ سند | ⏳ | ⭐ شامل `capacity` و `null` |
| ۵.۴ | `PlanAccuracyTest` — انحراف دوطرفه، نمونهٔ کم | ⏳ | |
| ۵.۵ | `ReportAuthTest` — منشی ۴۰۳، بازه ۴۲۲ | ⏳ | |
| ۵.۶ | `ReportQueryCountTest` — تعداد کوئری مستقل از تعداد منبع | ⏳ | |
## ۶. مستندات
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۶.۱ | `docs/api/reports.md` — معنی هر عدد + جدول `active_ratio` | ⏳ | |
| ۶.۲ | `docs/architecture/domain-events.md` — قرارداد، فهرست، outbox، idempotency | ⏳ | |
| ۶.۳ | جدول تفاوت `AppointmentEvent` با `DomainEventLog` | ⏳ | ⭐ وگرنه یکی حذف می‌شود |
## ۷. بازبینی پایانی
| # | مورد | وضعیت | یادداشت |
|---|---|---|---|
| ۷.۱ | هیچ 🔄 و ⏳ بی‌دلیل نمانده | ⏳ | |
| ۷.۲ | `bin/phpunit` کامل سبز | ⏳ | |
| ۷.۳ | `--group=slot-mode-frozen` سبز | ⏳ | |
| ۷.۴ | `phpstan` بدون خطای جدید | ⏳ | |
| ۷.۵ | `npx tsc --noEmit` و `yarn test` سبز | ⏳ | |
| ۷.۶ | تست‌های tenant سبز | ⏳ | |
| ۷.۷ | `docs/api/*` به‌روز | ⏳ | |
| ۷.۸ | چک‌لیست UI کامل | ⏳ | |
| ۷.۹ | پیامک‌های موجود سرتاسر تست شدند (outbox نشکستشان) | ⏳ | ⭐ |
| ۷.۱۰ | دو کلاینت دیگر بررسی شدند | ⏳ | |
| ۷.۱۱ | commit، سپس `graphify update .` | ⏳ | |
| ۷.۱۲ | موارد به‌تعویق با دلیل و تسک مقصد | ⏳ | `plan-accuracy` اگر داده نبود |