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:
@@ -0,0 +1,173 @@
|
||||
# نکات پیادهسازی — تسک ۱۲
|
||||
|
||||
## ۱. `book-all` همه یا هیچ
|
||||
|
||||
```php
|
||||
$this->em->wrapInTransaction(function () { /* همهٔ hold ها و confirm ها */ });
|
||||
```
|
||||
|
||||
اگر جلسهٔ ۵ وقت نداشت، جلسات ۱ تا ۴ هم rollback میشوند. رزرو نیمهکاره یعنی بیمار
|
||||
پیامک چهار نوبت میگیرد، فکر میکند دورهاش کامل رزرو شده، و چهار ماه بعد میفهمد نه.
|
||||
|
||||
⚠️ ولی رویدادها (پیامک) با `DispatchAfterCurrentBusStamp` بعد از commit میروند (تسک ۰۷)،
|
||||
پس در حالت rollback هیچ پیامکی نرفته. این وابستگی را جدی بگیر: اگر کسی در تسک ۰۷
|
||||
`dispatch` را قبل از commit گذاشته باشد، اینجا هشت پیامک اشتباه میرود.
|
||||
|
||||
## ۲. لنگر متحرک، نه تاریخ ثابت
|
||||
|
||||
```php
|
||||
// ❌ فاصله از شروع دوره
|
||||
$target = $course->getStartedAt() + $n * $idealDays * 86400;
|
||||
|
||||
// ✅ فاصله از جلسهٔ قبلی
|
||||
$anchor = $slot->start; // در هر تکرار حلقه بهروز میشود
|
||||
```
|
||||
|
||||
اگر جلسهٔ ۲ چهار روز دیرتر افتاد، جلسهٔ ۳ هم باید چهار روز جابهجا شود — وگرنه فاصلهٔ
|
||||
۲ به ۳ میشود ۲۴ روز و از حداقل ۲۱ رد نمیشود ولی از نظر درمانی غلط است.
|
||||
|
||||
## ۳. لنگر پیشنهاد بعدی: آخرین جلسهٔ **انجامشده**
|
||||
|
||||
```php
|
||||
private function lastCompletedAt(TreatmentCourse $course): ?int
|
||||
{
|
||||
// status = completed، نه booked
|
||||
return $this->sessionRepo->maxCompletedAt($course);
|
||||
}
|
||||
```
|
||||
|
||||
اگر از آخرین جلسهٔ `booked` حساب کنی، بیمار که نوبتش را لغو کرد یا نیامد، پیشنهاد بعدی
|
||||
غلط میشود. فقط جلسهٔ واقعاً انجامشده لنگر است.
|
||||
|
||||
جلسهٔ اول دوره: لنگر `time()` است، یا `started_at`.
|
||||
|
||||
## ۴. snapshot پروتکل
|
||||
|
||||
چهار فیلد فاصله و `params` هر جلسه کپی میشوند. تست:
|
||||
|
||||
```php
|
||||
// tests/Course/ProtocolSnapshotTest.php
|
||||
$course = $this->starter->start($patient, $service); // protocol: 8 جلسه، 28 روز
|
||||
$protocol->setIdealDays(14)->setSessionCount(4);
|
||||
$this->em->flush();
|
||||
|
||||
self::assertSame(28, $course->getIdealDays());
|
||||
self::assertCount(8, $course->getSessions());
|
||||
```
|
||||
|
||||
قانون پنجم مستند. بدون این، کلینیک که پروتکل را عوض کند، دورههای در جریان ۵۰ بیمار
|
||||
یکشبه بیمعنا میشوند.
|
||||
|
||||
## ۵. سقف ۹۰ روز و پیام روشن
|
||||
|
||||
۸ جلسه × ۲۸ روز = ۲۲۴ روز. جستجوی تسک ۰۶ فقط ۹۰ روز است. پس `book-all` معمولاً
|
||||
۳ تا ۴ جلسه رزرو میکند و بقیه `planned` میمانند.
|
||||
|
||||
پاسخ باید صریح بگوید:
|
||||
|
||||
```json
|
||||
{
|
||||
"booked_count": 3,
|
||||
"remaining_planned": 5,
|
||||
"message": "۳ جلسهٔ نخست رزرو شد. بقیهٔ جلسات خارج از بازهٔ مجاز رزرو (۹۰ روز) هستند و بعداً قابل رزروند."
|
||||
}
|
||||
```
|
||||
|
||||
بدون این پیام، کاربر فکر میکند سیستم خراب است.
|
||||
|
||||
## ۶. `same_as_previous` اجباری نیست
|
||||
|
||||
```php
|
||||
foreach ($preferred as $id) {
|
||||
if (in_array($id, $freeIds, true)) return $id;
|
||||
}
|
||||
return $this->fallback->pick(…); // ← نه throw
|
||||
```
|
||||
|
||||
اگر اپراتور جلسهٔ اول مرخصی است، بیمار نباید دو هفته منتظر بماند. ترجیح، نه الزام.
|
||||
اگر کلینیکی الزام واقعی داشت، آن یک قانون `resource` با `specific_resource` است (تسک ۰۹).
|
||||
|
||||
## ۷. تعامل با `spacing` تسک ۰۹
|
||||
|
||||
```php
|
||||
$effectiveMin = max($course->getMinDays(), $this->policies->minDaysFor($ctx) ?? 0);
|
||||
$effectiveMax = min($course->getMaxDays(), $this->policies->maxDaysFor($ctx) ?? PHP_INT_MAX);
|
||||
if ($effectiveMin > $effectiveMax) {
|
||||
throw new AppException(ErrorCodes::ERR_VALIDATION_001,
|
||||
'قوانین کلینیک با پروتکل این دوره سازگار نیستند', 422);
|
||||
}
|
||||
```
|
||||
|
||||
سختگیرانهتر برنده. و اگر ترکیبشان بازهٔ تهی ساخت، خطای روشن — نه جستجوی بینتیجه.
|
||||
|
||||
## ۸. اتصال به پکیج
|
||||
|
||||
اگر `TreatmentCourse.package` پر باشد، هر `confirm` جلسه یک واحد اعتبار مصرف میکند
|
||||
(تسک ۱۱). `book-all` هشت جلسه یعنی هشت مصرف — پس پیش از شروع:
|
||||
|
||||
```php
|
||||
if ($course->getPackage() !== null) {
|
||||
$balance = $this->ledger->balance($course->getPackage());
|
||||
if ($balance < count($plannedSessions)) {
|
||||
// خطا نیست — هشدار
|
||||
$result->addWarning(sprintf('اعتبار پکیج (%d) کمتر از جلسات باقیمانده (%d) است', $balance, $count));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
هشدار نه خطا: بیمار میتواند بقیه را نقدی بپردازد.
|
||||
|
||||
## ۹. edge case ها
|
||||
|
||||
| حالت | رفتار درست |
|
||||
|---|---|
|
||||
| دورهٔ فعال دوم برای همان سرویس | `422` با uuid دورهٔ موجود در `meta` |
|
||||
| لغو جلسهٔ وسط دوره | `CourseSession` → `planned`، `appointment_id` → NULL، بقیه دستنخورده |
|
||||
| عدم حضور (`no_show`) در جلسه | `CourseSession` → `skipped`؛ لنگر همان جلسهٔ قبلی میماند |
|
||||
| جلسهٔ آخر `completed` | دوره → `completed` خودکار + رویداد `CourseCompleted` |
|
||||
| `abandon` دورهٔ نیمهکاره | جلسات `booked` **لغو نمیشوند** خودکار — پاسخ شامل تعدادشان و لینک |
|
||||
| بیمار ۶۰ روز غیبت (> max) | `warning` در پیشنهاد؛ رزرو **مسدود نمیشود** |
|
||||
| پروتکل با `session_count = 1` | `422` — همان نوبت تکی است |
|
||||
| `params` با مقدار آرایه | `422` — فقط اسکالر |
|
||||
| حذف پروتکلی که دورهٔ فعال دارد | `422` (FK RESTRICT) — `active=false` مسیر درست |
|
||||
| دوره روی سرویسی که `bookable=false` شد | جلسات موجود میمانند؛ جلسهٔ جدید رزرو نمیشود، پیام روشن |
|
||||
|
||||
سطر «abandon» عمدی است: لغو خودکار هشت نوبت آیندهٔ بیمار بدون تأیید صریح، عملی
|
||||
برگشتناپذیر روی داده و ظرفیت کلینیک است. کاربر باید خودش تصمیم بگیرد.
|
||||
|
||||
## ۱۰. تست
|
||||
|
||||
```
|
||||
tests/Course/CourseStarterTest.php
|
||||
- ۸ جلسهٔ planned با params درست
|
||||
- سرویس بدون پروتکل → 422
|
||||
- دورهٔ فعال دوم → 422 با meta
|
||||
tests/Course/ProtocolSnapshotTest.php ← ⭐ قانون پنجم
|
||||
tests/Course/CourseSchedulerTest.php ← ⭐
|
||||
- book-all: لنگر متحرک (فاصله از جلسهٔ قبلی، نه از شروع)
|
||||
- نزدیکترین به ایدهآل انتخاب میشود، نه اولین
|
||||
- شکست جلسهٔ N → rollback همهٔ ۱..N-1
|
||||
- سقف ۹۰ روز → جلسات باقی planned + پیام
|
||||
tests/Course/NextSuggestionTest.php
|
||||
- لنگر = آخرین completed، نه booked
|
||||
- عبور از max → warning
|
||||
tests/Course/CourseProgressTest.php
|
||||
- completed/total/next_session_number/next_params
|
||||
tests/Course/SameResourcePreferenceTest.php
|
||||
- منبع جلسهٔ اول ترجیح داده میشود
|
||||
- منبع مشغول → fallback به least_gap، بدون خطا
|
||||
tests/Course/CoursePolicyInteractionTest.php
|
||||
- قانون سختگیرانهتر برنده
|
||||
- بازهٔ تهی → 422 روشن
|
||||
tests/Course/CoursePackageTest.php
|
||||
- هر جلسه یک واحد مصرف
|
||||
- اعتبار کمتر از جلسات → warning نه error
|
||||
tests/Course/CourseLifecycleTest.php
|
||||
- لغو وسط دوره · no_show → skipped · جلسهٔ آخر → completed خودکار
|
||||
- abandon نوبتهای booked را لغو نمیکند
|
||||
```
|
||||
|
||||
## ۱۱. مستندات
|
||||
|
||||
`docs/api/course.md` بساز. حتماً بنویس: قاعدهٔ «سختگیرانهتر برنده» بین پروتکل و قانون،
|
||||
رفتار سقف ۹۰ روز، و اینکه `abandon` نوبتها را لغو نمیکند.
|
||||
Reference in New Issue
Block a user