- 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.
7.5 KiB
نکات پیادهسازی — تسک ۰۵
۱. برنامه یک تابع خالص است
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 دو مقدار میدهد:
public readonly int $startOffset; // ۳۵ — چیزی که به بیمار نشان داده میشود
public readonly int $endOffset; // ۵۵
public readonly int $occupancyStartOffset; // ۳۰ — چیزی که تسک ۰۶/۰۷ استفاده میکند
public readonly int $occupancyEndOffset; // ۶۵
محاسبهشان اینجا انجام میشود چون setup/cleanup per منبع کاندید متفاوت است و باید
بعد از مرحلهٔ ۶ (پیدا کردن کاندیدها) حساب شود — با بیشینهٔ کاندیدها، تا برنامه محافظهکار
بماند و تسک ۰۶ بعد از انتخاب منبع دقیقش کند.
۴. ادغام: count بیشینه، نه جمع
وقتی دو بخش همنوع ادغام میشوند و هر دو «۱ اپراتور» میخواهند، نتیجه ۱ اپراتور است، نه ۲. همان اپراتور هر دو کار را میکند.
جمع فقط وقتی درست است که دو کار واقعاً همزمان انجام شوند — که در بخشهای ادغامشده (که ذاتاً یک کار شدهاند) صدق نمیکند.
۵. قید جنسیت — سکوت ممنوع
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.