- 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.
8.2 KiB
نکات پیادهسازی — تسک ۱۲
۱. 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 |
| لغو جلسهٔ وسط دوره | 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 نوبتها را لغو نمیکند.