Files
clinicpro/docs/new_feture/taskes/task-05-appointment-plan/implementation_notes.md
T
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
2026-07-30 11:43:58 +03:30

132 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نکات پیاده‌سازی — تسک ۰۵
## ۱. برنامه یک تابع خالص است
`AppointmentPlanBuilder::build()` نباید چیزی بنویسد، چیزی cache کند، یا به `time()` نگاه کند.
ورودی یکسان → خروجی یکسان. دلیلش تسک ۰۶ است: موتور جستجو یک برنامه می‌سازد و آن را
برای ۹۰ روز × ده‌ها نقطهٔ شروع استفاده می‌کند. اگر ساختن برنامه عوارض جانبی داشته باشد،
جستجو یا کند می‌شود یا نتیجهٔ ناپایدار می‌دهد.
تنها I/O مجاز: خواندن الگوها و منابع کاندید (مرحلهٔ ۶).
## ۲. `offset_minutes` نسبی است، نه مطلق
بخش‌ها با فاصله از **شروع نوبت** ذخیره می‌شوند، نه timestamp:
```
بخش ۱ — offset 0, duration 5
بخش ۲ — offset 5, duration 30
بخش ۳ — offset 35, duration 20
بخش ۴ — offset 55, duration 5
```
تسک ۰۶ همین برنامه را روی هر نقطهٔ شروع کاندید «می‌لغزاند». اگر offset مطلق بود، برای هر
نقطهٔ شروع باید برنامه از نو ساخته می‌شد.
## ۳. `setup/cleanup` منبع کجا اعمال می‌شود
**نه در برنامه، در اشغال.** برنامه می‌گوید «اپراتور از دقیقهٔ ۳۵ تا ۵۵ لازم است». اشغال
واقعی همان منبع، با `setup=5, cleanup=10`، از دقیقهٔ ۳۰ تا ۶۵ است.
پس `PlannedRequirement` دو مقدار می‌دهد:
```php
public readonly int $startOffset; // ۳۵ — چیزی که به بیمار نشان داده می‌شود
public readonly int $endOffset; // ۵۵
public readonly int $occupancyStartOffset; // ۳۰ — چیزی که تسک ۰۶/۰۷ استفاده می‌کند
public readonly int $occupancyEndOffset; // ۶۵
```
محاسبه‌شان اینجا انجام می‌شود چون `setup/cleanup` per منبع کاندید متفاوت است و باید
بعد از مرحلهٔ ۶ (پیدا کردن کاندیدها) حساب شود — با بیشینهٔ کاندیدها، تا برنامه محافظه‌کار
بماند و تسک ۰۶ بعد از انتخاب منبع دقیقش کند.
## ۴. ادغام: `count` بیشینه، نه جمع
وقتی دو بخش هم‌نوع ادغام می‌شوند و هر دو «۱ اپراتور» می‌خواهند، نتیجه **۱** اپراتور است،
نه ۲. همان اپراتور هر دو کار را می‌کند.
جمع فقط وقتی درست است که دو کار واقعاً هم‌زمان انجام شوند — که در بخش‌های ادغام‌شده
(که ذاتاً یک کار شده‌اند) صدق نمی‌کند.
## ۵. قید جنسیت — سکوت ممنوع
```php
if (($req->constraints['same_gender_as_patient'] ?? false) && $patient?->getGender() === null) {
throw new AppException(
ErrorCodes::ERR_VALIDATION_001,
'برای این سرویس ثبت جنسیت بیمار الزامی است',
422
);
}
```
نادیده گرفتنِ قید وقتی داده نیست، بدترین حالت است: در کلینیک زیبایی ایران این یک الزام
جدی است (مستند بند ۶) و نقض خاموشش یعنی بیمار سر قرار با اپراتور نامناسب روبه‌رو می‌شود.
## ۶. `occupancy` سه حالت — تفاوت عملی
| حالت | در تسک ۰۶ | در تسک ۰۷ |
|---|---|---|
| `exclusive` | منبع باید کاملاً آزاد باشد | یک ردیف اشغال با `units = capacity` |
| `shared` | `COUNT(اشغال‌های فعال) < capacity` | یک ردیف با `units = 1` |
| `passive` | مثل `exclusive` | ردیف اشغال با پرچم `passive` — گزارش بهره‌وری آن را «کارِ فعال» حساب نمی‌کند |
اینجا فقط ستون و اعتبارسنجی است؛ معنی‌شان در ۰۶ و ۰۷ پیاده می‌شود. ولی تفاوت را همین
حالا در `docs/api/appointment-plan.md` بنویس.
## ۷. سقف‌ها — حفاظت از تسک ۰۶
| سقف | مقدار | چرا |
|---|---|---|
| تعداد بخش یک برنامه | ۲۰ | هر بخش یعنی یک بررسی تداخل per نقطهٔ شروع |
| مجموع مدت برنامه | ۴۸۰ دقیقه | برنامهٔ طولانی‌تر عملاً هیچ روزی جا نمی‌شود |
| تعداد نیازمندی هر بخش | ۱۰ | |
| تعداد آیتم انتخابی | ۲۰ | (تسک ۰۴ هم `max_select` دارد، این سقف کلی است) |
نقض → `422` با پیام روشن، نه تلاش برای محاسبه. مستند بند ۱۷ ریسک «کند شدن جستجو با
زیاد شدن منابع» را اولین ریسک می‌داند؛ سقف‌ها ارزان‌ترین دفاع‌اند.
## ۸. edge case ها
| حالت | رفتار درست |
|---|---|
| سرویس بدون الگو | یک بخش مجازی با منبع `type=doctor` — رفتار امروزی |
| بخش با `requirements = []` | معتبر؛ زمان می‌گیرد، منبع نمی‌گیرد |
| همهٔ بخش‌ها `fixed_minutes` و آیتم‌ها مدت دارند | مدت آیتم‌ها **نادیده** نمی‌رود: اگر هیچ بخش سهمی نباشد و آیتم مدت داشته باشد → `422` هنگام ذخیرهٔ الگو |
| `duration_share` جمعش ۹۹ | `422` هنگام ذخیره |
| دو بخش با یک `sequence` | ترتیب بر اساس `id` پایدار شود، نه خطا |
| بخش آیتمی که آیتمش انتخاب نشده | در برنامه نمی‌آید |
| `specific_resource` غیرفعال | `candidates=[]` → خطای انسانی |
| استخر خالی | همان |
| بیمار مهمان (بدون پرونده) و قید جنسیت | جنسیت از فرم رزرو (`patient_gender` روی `Appointment` هست) گرفته شود |
## ۹. تست
```
tests/Appointment/Plan/SegmentAssemblerTest.php
- جمع‌آوری بخش سرویس + بخش آیتم
- ادغام دو بخش هم‌نوع mergeable → یکی، count بیشینه نه جمع
- بخش غیر-mergeable هم‌نوع → جدا می‌ماند
tests/Appointment/Plan/SegmentDurationResolverTest.php
- fixed ثابت می‌ماند وقتی آیتم‌ها زیاد شوند
- سهمی با مدت محاسبه‌شدهٔ تسک ۰۴ مقیاس می‌گیرد
- جمع مدت بخش‌ها = total_minutes
tests/Appointment/Plan/AppointmentPlanBuilderTest.php
- سناریوی کامل مستند: چهار بخش، offset های ۰/۵/۳۵/۵۵، total=60
- سرویس بدون الگو → یک بخش با منبع doctor (سازگاری)
- قطعیت: دو بار build با ورودی یکسان → خروجی یکسان
tests/Appointment/Plan/RequirementResolverTest.php
- مهارت موجود → کاندید
- هیچ کاندید → NoEligibleResourceException با پیام شامل نقش و مهارت
- قید جنسیت با بیمار بدون جنسیت → 422
- منبع محیط دیگر هرگز کاندید نمی‌شود
tests/Appointment/Plan/PlanLimitsTest.php
- ۲۱ بخش → 422 · ۴۸۱ دقیقه → 422
```
## ۱۰. مستندات
`docs/api/appointment-plan.md` بساز — شامل جدول سه حالت اشغال، مثال کامل خروجی
`preview`، و توضیح تفاوت `offset` نمایشی با `occupancy_offset`.