feat: implement cancellation policy, no-show tracking, and waitlist management

- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
This commit is contained in:
hamed
2026-07-30 11:43:58 +03:30
parent 1d338503c8
commit 021d0eb6b2
62 changed files with 8098 additions and 0 deletions
@@ -0,0 +1,89 @@
# تسک ۰۴ — کاتالوگ خدمات نسخهٔ ۲: گروه آیتم، دو نوع زمان، ناسازگاری
**فاز:** ۱ (هسته) · **وابستگی:** ۰۱ · **زمان:** ۱۴-۱۶ ساعت
---
## هدف
مستند بند ۵ می‌گوید انتخاب آیتم خودش قانون دارد و این قوانین **نباید** به موتور قوانین
سپرده شوند: «حتماً یک سطح انرژی، فقط یکی»، «بین ۱ تا ۸ دندان»، «بیکینی با فول‌بادی
جمع نمی‌شود». و مهم‌تر: هر آیتم دو زمان دارد — «زمان تنها» و «زمان اضافه».
## وضعیت فعلی
```php
// src/ClinicService/Entity/ServiceItem.php
private ?int $durationMinutes = null; // یک عدد، تخت
private int $priceRials = 0;
private bool $bookable = false;
```
و در `AppointmentController::serviceSlots()`:
```php
$totalMinutes += $duration; // ← جمع ساده؛ همان فرمولی که مستند ردش می‌کند
```
نتیجه: بیمار که «صورت + بیکینی» می‌خواهد، ۱۵+۱۵=۳۰ دقیقه ظرفیت می‌گیرد در حالی که
واقعیت ۱۵+۸=۲۳ دقیقه است. هفت دقیقه ضرب در روزی ۲۰ نوبت = یک ساعت ظرفیت هدررفته در روز.
همچنین `ServiceSection` تک‌سطحی است و دسته‌بندی درختی مستند را ندارد.
## دامنه
**هست:**
- `ServiceCategory` درختی (جدا از `ServiceSection` موجود که «بخش کلینیک» است)
- `ItemGroup` با `min_select` / `max_select`
- روی `ServiceItem`: `solo_duration_minutes` و `additional_duration_minutes`
- `ServiceItemRelation` برای `incompatible_with` و `requires`
- `ServiceBranchOverride` برای قیمت و مدت اختصاصی شعبه
- `session_count` روی سرویس (تک‌جلسه یا دوره‌ای — پروتکل کاملش تسک ۱۲)
- `ServiceSelectionValidator` — اعتبارسنجی انتخاب کاربر پیش از هر محاسبه
- `DurationCalculator` — محاسبهٔ درست مدت با دو نوع زمان
**نیست:** بخش‌های نوبت و نیازمندی منبع (تسک ۰۵)، اعمال روی جستجوی وقت (تسک ۰۶).
## Endpoint ها
| متد | مسیر | توضیح |
|---|---|---|
| GET | `/api/v1/service-categories/tree` | درخت دسته‌بندی |
| POST/PATCH/DELETE | `/api/v1/service-category[/{uuid}]` | |
| GET/POST | `/api/v1/service-item/{uuid}/groups` | گروه‌های آیتم یک سرویس |
| PATCH/DELETE | `/api/v1/item-group/{uuid}` | |
| PUT | `/api/v1/item-group/{uuid}/items` | جایگزینی کامل آیتم‌های گروه |
| PUT | `/api/v1/service-item/{uuid}/relations` | ناسازگاری و پیش‌نیاز |
| PUT | `/api/v1/service-item/{uuid}/branch-overrides` | قیمت/مدت per شعبه |
| POST | `/api/v1/service-selection/validate` | اعتبارسنجی انتخاب + مدت و قیمت محاسبه‌شده |
`POST /service-selection/validate` مهم‌ترین endpoint این تسک است: سایت عمومی و پنل هر دو
پیش از رفتن به مرحلهٔ انتخاب زمان، آن را صدا می‌زنند.
## معیار پذیرش
- ✅ موفق: سرویس «لیزر» با گروه «نواحی» (`min=1, max=8`) و آیتم‌های صورت (تنها ۱۵، اضافه ۸)
و بیکینی (تنها ۱۲، اضافه ۸). انتخاب هر دو →
`POST /service-selection/validate` برمی‌گرداند `total_duration_minutes = 23`
(اولین آیتم زمان تنها، بقیه زمان اضافه) و `valid = true`.
- ✅ موفق: انتخاب فقط بیکینی → `total_duration_minutes = 12`.
- ✅ موفق: شعبهٔ مرکزی برای همین سرویس `price_rials` بالاتر دارد →
با `branch_uuid` مرکزی، قیمت override اعمال می‌شود.
- ❌ خطا: انتخاب صفر آیتم از گروهی با `min_select=1``valid=false` با
`errors[{group_uuid, code: 'min_select', message: 'انتخاب حداقل یک مورد از «نواحی» الزامی است'}]`.
- ❌ خطا: انتخاب ۹ آیتم از گروهی با `max_select=8``valid=false` با کد `max_select`.
- ❌ خطا: انتخاب دو آیتم ناسازگار → `valid=false` با کد `incompatible` و نام هر دو آیتم.
- ❌ خطا: انتخاب آیتمی که پیش‌نیازش انتخاب نشده → `valid=false` با کد `missing_prerequisite`.
- ❌ خطا: uuid آیتم از محیط دیگر → `404` (نه ۴۲۲ — نباید وجودش لو برود).
- ⚠️ مرزی: `max_select = null` یعنی نامحدود.
- ⚠️ مرزی: گروه با `min_select = 0` یعنی اختیاری.
- ⚠️ مرزی: آیتم بدون `additional_duration_minutes` → از `solo_duration_minutes` استفاده شود
(سازگاری با داده‌های موجود که فقط یک `duration_minutes` دارند).
- ⚠️ مرزی: حلقهٔ پیش‌نیاز (الف پیش‌نیاز ب، ب پیش‌نیاز الف) → `422` هنگام ثبت رابطه.
## خروجی
- توسعهٔ `src/ClinicService/` (بدون شکستن endpoint های موجود)
- `assets/admin/pages/ServiceDetailPage.tsx` توسعه: تب «گروه‌ها و آیتم‌ها»
- `docs/api/clinic-services.md` به‌روزرسانی
- migration + backfill: `duration_minutes` موجود → `solo_duration_minutes`