Files
clinicpro/docs/new_feture/taskes/task-12-treatment-course/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

8.2 KiB
Raw Blame History

نکات پیاده‌سازی — تسک ۱۲

۱. book-all همه یا هیچ

$this->em->wrapInTransaction(function () { /* همهٔ hold ها و confirm ها */ });

اگر جلسهٔ ۵ وقت نداشت، جلسات ۱ تا ۴ هم rollback می‌شوند. رزرو نیمه‌کاره یعنی بیمار پیامک چهار نوبت می‌گیرد، فکر می‌کند دوره‌اش کامل رزرو شده، و چهار ماه بعد می‌فهمد نه.

⚠️ ولی رویدادها (پیامک) با DispatchAfterCurrentBusStamp بعد از commit می‌روند (تسک ۰۷)، پس در حالت rollback هیچ پیامکی نرفته. این وابستگی را جدی بگیر: اگر کسی در تسک ۰۷ dispatch را قبل از commit گذاشته باشد، اینجا هشت پیامک اشتباه می‌رود.

۲. لنگر متحرک، نه تاریخ ثابت

// ❌ فاصله از شروع دوره
$target = $course->getStartedAt() + $n * $idealDays * 86400;

// ✅ فاصله از جلسهٔ قبلی
$anchor = $slot->start;   // در هر تکرار حلقه به‌روز می‌شود

اگر جلسهٔ ۲ چهار روز دیرتر افتاد، جلسهٔ ۳ هم باید چهار روز جابه‌جا شود — وگرنه فاصلهٔ ۲ به ۳ می‌شود ۲۴ روز و از حداقل ۲۱ رد نمی‌شود ولی از نظر درمانی غلط است.

۳. لنگر پیشنهاد بعدی: آخرین جلسهٔ انجام‌شده

private function lastCompletedAt(TreatmentCourse $course): ?int
{
    // status = completed، نه booked
    return $this->sessionRepo->maxCompletedAt($course);
}

اگر از آخرین جلسهٔ booked حساب کنی، بیمار که نوبتش را لغو کرد یا نیامد، پیشنهاد بعدی غلط می‌شود. فقط جلسهٔ واقعاً انجام‌شده لنگر است.

جلسهٔ اول دوره: لنگر time() است، یا started_at.

۴. snapshot پروتکل

چهار فیلد فاصله و params هر جلسه کپی می‌شوند. تست:

// 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 می‌مانند.

پاسخ باید صریح بگوید:

{
  "booked_count": 3,
  "remaining_planned": 5,
  "message": "۳ جلسهٔ نخست رزرو شد. بقیهٔ جلسات خارج از بازهٔ مجاز رزرو (۹۰ روز) هستند و بعداً قابل رزروند."
}

بدون این پیام، کاربر فکر می‌کند سیستم خراب است.

۶. same_as_previous اجباری نیست

foreach ($preferred as $id) {
    if (in_array($id, $freeIds, true)) return $id;
}
return $this->fallback->pick();      // ← نه throw

اگر اپراتور جلسهٔ اول مرخصی است، بیمار نباید دو هفته منتظر بماند. ترجیح، نه الزام. اگر کلینیکی الزام واقعی داشت، آن یک قانون resource با specific_resource است (تسک ۰۹).

۷. تعامل با spacing تسک ۰۹

$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 هشت جلسه یعنی هشت مصرف — پس پیش از شروع:

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
لغو جلسهٔ وسط دوره CourseSessionplanned، appointment_id → NULL، بقیه دست‌نخورده
عدم حضور (no_show) در جلسه CourseSessionskipped؛ لنگر همان جلسهٔ قبلی می‌ماند
جلسهٔ آخر 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 نوبت‌ها را لغو نمی‌کند.