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

7.5 KiB
Raw Blame History

نکات پیاده‌سازی — تسک ۰۵

۱. برنامه یک تابع خالص است

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.