Files
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.1 KiB
Raw Permalink Blame History

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

۱. ServiceItem را تغییر نام نده

در این جدول‌ها و کدها به آن ارجاع هست:

appointment_service_items · service_item_staff · service_item_consumables
service_item_audit_logs   · tariffs.service_item_id · session_services
appointments.service_item_id

و در nobat724_front/services/response.js و clinic-pro-tauri/src/service/response.js کلید service_item_uuid در بدنهٔ رزرو می‌رود. تغییر نام یعنی شکستن سه ریپو بدون یک خطای build. کلاس جدید ServiceOption بساز و در docs/api/clinic-services.md جدول واژگان (فایل architecture) را عیناً بنویس.

۲. /service-selection/validate هم عمومی است هم پنلی

سایت عمومی بدون توکن آن را صدا می‌زند (بیمار هنوز وارد نشده). پس:

  • در security.yaml مسیرش را whitelist کن
  • بدون کاربر احراز شده، TenantFilter خاموش است → گارد دستی اجباری است: محیط از doctor_uuid + clinic_uuid درخواست حل می‌شود و همهٔ uuid ها با TenantOwnershipChecker::belongsToPair() سنجیده می‌شوند
  • نرخ‌محدودسازی: این endpoint یک enumerate کنندهٔ کاتالوگ است. symfony/rate-limiter روی IP، مثل بقیهٔ endpoint های عمومی

این دقیقاً همان اشتباهی است که یک بار در GET /api/v1/appointment-service-slots رخ داد و در فاز ۸ tenancy رفع شد. تکرارش نکن.

۳. قطعیت محاسبهٔ مدت

دو انتخاب یکسان با ترتیب متفاوت باید همیشه یک عدد بدهند. تست:

$a = $calc->totalMinutes($service, [$face, $bikini]);
$b = $calc->totalMinutes($service, [$bikini, $face]);
self::assertSame($a, $b);

اگر این تست نباشد، اولین بهینه‌سازی که ترتیب آرایه را عوض کند، قیمت‌ها را تغییر می‌دهد و هیچ‌کس نمی‌فهمد چرا.

۴. تصمیم: مرتب‌سازی نزولی بر اساس «زمان تنها»

مستند نگفته کدام آیتم «اولی» است. سه گزینه بررسی شد:

گزینه مشکل
ترتیب انتخاب کاربر غیرقطعی — همان انتخاب دو مدت می‌دهد
sort_order تعریف‌شده کلینیک باید برای هر ترکیب فکر کند؛ عملاً پر نمی‌شود
بیشترین «زمان تنها» قطعی، بدون ورودی اضافه، و از نظر کسب‌وکار درست: کار بزرگ‌تر آماده‌سازی را می‌بلعد

انتخاب سوم. دلیلش را در کد به‌صورت کامنت بنویس، وگرنه اولین بازبینی‌کننده آن را «مرتب‌سازی بی‌دلیل» می‌بیند و حذفش می‌کند.

۵. additional_minutes = null یعنی محافظه‌کار

$rest->getAdditionalMinutes() ?? $rest->getSoloMinutes()

نه صفر. اگر null را صفر بگیری، سرویس‌های موجود که این ستون را ندارند یک‌شبه مدتشان نصف می‌شود و ظرفیت الکی باز می‌شود — یعنی نوبت روی نوبت.

۶. ناسازگاری متقارن، پیش‌نیاز جهت‌دار

// ناسازگاری — یک ردیف کافی است، هر دو جهت پرس‌وجو می‌شوند
$conflicts = $relationRepo->createQueryBuilder('r')
    ->where('r.type = :incompatible')
    ->andWhere('r.source IN (:sel) AND r.target IN (:sel)')
    ->setParameter('sel', $selectedIds)
    ->getQuery()->getResult();

پیش‌نیاز جهت‌دار است و حلقه ممنوع. assertNoCycle() با DFS هنگام ثبت اجرا شود، نه هنگام اعتبارسنجی انتخاب — بررسی حلقه در مسیر داغ رزرو، هزینهٔ بی‌دلیل است.

۷. عمق درخت و حذف دسته

  • سقف عمق ۴ (depth <= 3 با ریشهٔ صفر)
  • حذف دسته‌ای که فرزند یا سرویس دارد → 422
  • جابه‌جایی دسته → path همهٔ نوادگان با یک UPDATE … SET path = REPLACE(path, :old, :new) به‌روز شود، در یک تراکنش

۸. edge case ها

حالت رفتار درست
سرویس بدون هیچ گروه معتبر — رفتار امروزی، مدت = duration_minutes
گروه بدون هیچ آیتم فعال و min_select=1 انتخاب همیشه نامعتبر می‌شود → هشدار در پنل هنگام ذخیره
min_select > max_select 422
max_select بزرگ‌تر از تعداد آیتم‌های فعال مجاز؛ عملاً یعنی نامحدود
additional_minutes > solo_minutes 422
آیتم غیرفعال در انتخاب 422 با کد inactive_option
override شعبه با bookable=0 سرویس در آن شعبه در لیست رزرو نیاید
دو آیتم ناسازگار در دو گروه مختلف همچنان ناسازگار — رابطه بین آیتم‌هاست، نه گروه‌ها
انتخاب آیتم از سرویس دیگر 422 option_not_in_service (بعد از بررسی tenant)

۹. تست

tests/ClinicService/DurationCalculatorTest.php
  - یک آیتم → solo
  - دو آیتم یک گروه → solo(بزرگ‌تر) + additional(کوچک‌تر)
  - دو گروه → هر گروه solo خودش
  - additional=null → از solo استفاده شود
  - قطعیت: جابه‌جایی ترتیب ورودی، همان عدد
tests/ClinicService/ServiceSelectionValidatorTest.php
  - min_select نقض → کد min_select
  - max_select نقض → کد max_select
  - ناسازگار → کد incompatible با نام هر دو
  - پیش‌نیاز غایب → کد missing_prerequisite
  - چند خطا هم‌زمان → همه با هم برگردند
  - uuid محیط دیگر → 404 و هیچ اطلاعاتی در بدنه
tests/ClinicService/ServicePriceResolverTest.php
  - override شعبه بر تعرفه اولویت دارد
  - override جزئی (فقط قیمت) مدت را دست نمی‌زند
tests/ClinicService/ServiceCategoryTreeTest.php
  - عمق ۵ → 422 · حذف دستهٔ دارای فرزند → 422 · جابه‌جایی path نوادگان
tests/ClinicService/BackwardCompatibilityTest.php
  - سرویس بدون گروه: appointment-service-slots دقیقاً همان خروجی قبلی

آخرین تست مهم‌ترین است: این تسک نباید رفتار نوبت‌دهی سرویسی فعلی را تغییر دهد.

۱۰. مستندات

docs/api/clinic-services.md به‌روزرسانی با جدول واژگان + endpoint های جدید. یادآوری: قرارداد POST /service-selection/validate را nobat724_front مصرف می‌کند و شکستنش در build خطا نمی‌دهد.