Files
clinicpro/docs/api/course.md
T
hamedandClaude Opus 5 e9e61adfee feat(course): show how the course is actually going, not just how it was planned
Three gaps on the treatment-course page, all of them about the difference
between the protocol and reality.

The sessions table listed each date but not the gap between them, leaving the
operator to subtract two Jalali dates in their head. It now shows the real gap
and colours it as a warning past the protocol maximum.

A course cancelled mid-way stretches silently: the session goes back to
planned and nobody is told. The suggestion endpoint does warn, but only once a
branch is picked, so the warning could go unseen indefinitely. The page now
derives "N days since the last session, past the protocol maximum" from the
course itself, so it shows immediately.

The course's preferred resource was applied by the engine but never named in
the UI. The API now returns preferred_resource_name alongside the uuid, and
the text says plainly that it is a preference — the engine moves it up the
list, it does not hold the slot.

Two backend tests that were owed: the stricter of the protocol spacing and a
spacing policy wins (protocol 7 days, policy 21, effective 21 — otherwise a
clinic's safety rule could be bypassed by writing a short protocol), and a
session whose earliest possible date falls outside the 90-day horizon is
skipped rather than failing book-all, leaving the course untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 13:51:34 +03:30

12 KiB
Raw Blame History

Course — دورهٔ درمان

اندپوینت‌های src/Course/*. «لیزر معمولاً شش تا هشت جلسه است»؛ طراحی قبلی فقط نوبت تکی می‌شناخت، در حالی که دوره حالت اصلی کسب‌وکار است.

همهٔ مسیرها IS_AUTHENTICATED_FULLY می‌خواهند و به محیط جاری محدودند (404 برای محیط دیگر).


مفاهیم

چیست
CourseProtocol تعریف دوره per سرویس: تعداد جلسه و سه فاصلهٔ حداقل/ایده‌آل/حداکثر
CourseProtocolStep پارامتر هر جلسه (مثلاً سطح انرژی) — اسکالر و آزاد
TreatmentCourse دورهٔ یک بیمار؛ چهار عدد پروتکل را کپی می‌کند
CourseSession جلسات دوره: plannedbookedcompleted (یا skipped)

سه فاصله سه معنا دارند: min زودترین زمان مجاز، ideal بهترین، max جایی که دیرتر از آن اثر دوره افت می‌کند. برنامه‌ریز نزدیک‌ترین وقت به ایده‌آل را می‌گیرد، نه اولین وقت خالی: روز ۲۱ (حداقل) از نظر درمانی بدتر از روز ۲۷ است.

لنگر متحرک: فاصله همیشه از آخرین جلسهٔ انجام‌شده حساب می‌شود، نه از شروع دوره. اگر جلسهٔ ۲ سه روز دیرتر افتاد، جلسهٔ ۳ هم جابه‌جا می‌شود.

snapshot: تعداد جلسه، سه فاصله و پارامتر هر جلسه در لحظهٔ شروع کپی می‌شوند. تغییر پروتکل فردا، دورهٔ در جریان را عوض نمی‌کند (قانون پنجم مستند).

یک دورهٔ فعال per (بیمار، سرویس): با ستون active_course_key که در حالت‌های completed/abandoned تهی می‌شود — همان الگوی Appointment::activeSlotKey، چون MariaDB کلید یکتای جزئی ندارد.


GET · POST /api/v1/course-protocols

Request Body (POST)

{
  "service_uuid": "acea173f-aa5d-4d1e-bc19-9b5ca7e66234",
  "session_count": 8,
  "min_days": 21,
  "ideal_days": 28,
  "max_days": 45,
  "prefer_same_resource": true,
  "steps": [
    { "session_number": 1, "params": { "energy": 12 } },
    { "session_number": 2, "params": { "energy": 14 }, "override_duration_minutes": 45 }
  ]
}
Field Type Required Description
service_uuid string یک پروتکل per سرویس
session_count int حداقل ۲ — دورهٔ یک‌جلسه‌ای همان نوبت تکی است
min_days / ideal_days / max_days int باید min ≤ ideal ≤ max
prefer_same_resource bool پیش‌فرض true
steps array جایگزینی کامل؛ params فقط اسکالر

Response 201

{
  "success": true,
  "data": {
    "uuid": "6be5047e-0a3e-4c55-a637-77b814b3b8e3",
    "service_uuid": "acea173f-…",
    "service_name": "لیزر فول‌بادی",
    "session_count": 8,
    "min_days": 21,
    "ideal_days": 28,
    "max_days": 45,
    "prefer_same_resource": true,
    "active": true,
    "steps": [
      { "session_number": 1, "params": { "energy": 12 }, "override_duration_minutes": null },
      { "session_number": 2, "params": { "energy": 14 }, "override_duration_minutes": null }
    ],
    "created_at": 1785484481
  }
}

Errors

Code HTTP Description
ERR_VALIDATION_001 422 session_count < 2، ترتیب فاصله‌ها نادرست، سرویسی که از قبل پروتکل دارد، یا session_number بیرون بازه
ERR_VALIDATION_002 422 service_uuid غایب
ERR_NOT_FOUND_001 404 سرویس خارج از محیط جاری

PATCH /api/v1/course-protocol/{uuid} همان فیلدها؛ DELETE پروتکل را غیرفعال می‌کند چون دوره‌های در جریان به آن ارجاع دارند.


POST /api/v1/treatment-course

شروع دوره. همهٔ جلسات همان لحظه با وضعیت planned ساخته می‌شوند تا بیمار از روز اول ببیند «۸ جلسه» یعنی چه.

Request Body

{
  "patient_uuid": "db7bde5a-…",
  "protocol_uuid": "6be5047e-…",
  "patient_package_uuid": "dcd21f8e-…"
}

patient_package_uuid اختیاری است (تسک ۱۱). پکیجی که این خدمت را پوشش ندهد 422 می‌گیرد.

Response 201

{
  "success": true,
  "data": {
    "uuid": "8088fdd5-f19f-483e-b21b-11118e1b5e29",
    "patient_uuid": "db7bde5a-…",
    "service_uuid": "acea173f-…",
    "service_name": "لیزر فول‌بادی",
    "protocol_uuid": "6be5047e-…",
    "session_count": 8,
    "min_days": 21,
    "ideal_days": 28,
    "max_days": 45,
    "patient_package_uuid": null,
    "preferred_resource_uuid": null,
    "preferred_resource_name": null,
    "status": "active",
    "abandon_reason": null,
    "started_at": 1785484481,
    "completed_at": null,
    "progress": {
      "completed": 0,
      "booked": 0,
      "planned": 8,
      "skipped": 0,
      "total": 8,
      "next_session_number": 1,
      "next_params": { "energy": 12 },
      "last_completed_at": null
    },
    "sessions": [
      {
        "uuid": "08c4b98a-…",
        "session_number": 1,
        "params": { "energy": 12 },
        "appointment_uuid": null,
        "slot_start": null,
        "status": "planned",
        "completed_at": null
      },
      { "session_number": 3, "params": {}, "…": "جلسه‌ای که پروتکل برایش پارامتر ندارد" }
    ]
  }
}

preferred_resource_name نام همان منبع است و فقط برای نمایش می‌آید — پنل با آن روی پیشنهاد جلسهٔ بعدی می‌نویسد کدام دستگاه ترجیح داده می‌شود. ترجیح است نه الزام: موتور آن را جلوتر می‌آورد ولی اگر آزاد نباشد منبع دیگری می‌دهد، و متن UI هم همین را می‌گوید تا انتظار اشتباه نسازد.

Errors

Code HTTP Description
ERR_VALIDATION_001 422 دورهٔ فعال دیگری برای همین سرویس هست — پیام شناسهٔ آن دوره را می‌دهد
ERR_VALIDATION_002 422 patient_uuid یا protocol_uuid غایب
ERR_NOT_FOUND_001 404 بیمار، پروتکل یا پکیج خارج از محیط جاری
{
  "success": false,
  "data": null,
  "errors": [{
    "code": "ERR_VALIDATION_001",
    "message": "این بیمار یک دورهٔ فعال برای همین خدمت دارد (8088fdd5-f19f-483e-b21b-11118e1b5e29)",
    "field": "course_uuid"
  }]
}

GET /api/v1/treatment-course/{uuid} · /api/v1/patient/{uuid}/courses

همان بدنهٔ بالا. patient/{uuid}/courses جلسات را نمی‌دهد، فقط دوره + progress.


GET /api/v1/treatment-course/{uuid}/next-slot-suggestion

Query Type Required
branch_uuid string

Response 200

{
  "success": true,
  "data": {
    "session_number": 4,
    "params": { "energy": 18 },
    "ideal_at": 1787903681,
    "range": { "min": 1787298881, "max": 1789372481 },
    "suggested_slots": [{ "start": 1787903681, "end": 1787905481 }],
    "warning": null
  }
}

warning وقتی پر می‌شود که از حداکثر فاصله عبور شده باشد:

"از حداکثر فاصلهٔ مجاز (45 روز) عبور شده است. برای ادامهٔ دوره با پزشک مشورت کنید."

فاصلهٔ مؤثر سخت‌گیرانه‌ترین بین پروتکل دوره و قانون spacing (تسک ۰۹) است: قانون کلینیک نباید با پروتکل بجنگد، هر کدام سخت‌گیرتر بود همان اجرا می‌شود.

زمان گذشته پیشنهاد نمی‌شود؛ بیمارِ دیرکرده از همین حالا وقت می‌گیرد.


POST /api/v1/treatment-course/{uuid}/book-all

رزرو همهٔ جلسات باقی‌مانده.

Request Body

{ "branch_uuid": "…", "doctor_uuid": "…" }

Response 200

{
  "success": true,
  "data": {
    "booked": 3,
    "remaining": 5,
    "message": "5 جلسه بیرون از بازهٔ 90 روزهٔ رزرو افتاد و برنامه‌ریزی‌شده ماند؛ نزدیک‌تر که شدیم رزروشان کنید.",
    "course": { "…": "همان بدنهٔ دوره" }
  }
}

سه قاعده:

۱. همه یا هیچ — کل حلقه در یک تراکنش. اگر برای جلسهٔ ۵ وقتی نبود، جلسات ۱ تا ۴ هم rollback می‌شوند و 422 با شمارهٔ جلسهٔ مشکل‌دار برمی‌گردد. رزرو نیمه‌کاره بدترین حالت است: بیمار فکر می‌کند دوره‌اش رزرو شده و نصفش نیست. ۲. لنگر متحرک — هر جلسه از جلسهٔ قبلی فاصله می‌گیرد. ۳. افق ۹۰ روز — جلساتی که بیرون بازهٔ جستجو می‌افتند planned می‌مانند و در message گزارش می‌شوند. این خطا نیست.

Errors

Code HTTP Description
ERR_VALIDATION_001 422 برای یکی از جلسات وقتی در بازهٔ مجاز نبود
ERR_VALIDATION_002 422 branch_uuid یا doctor_uuid غایب

POST /api/v1/treatment-course/{uuid}/abandon

{ "reason": "انصراف بیمار" }

دلیل اجباری است. دورهٔ رهاشده جا را برای دورهٔ تازهٔ همان سرویس باز می‌کند.

⚠️ رهاکردن دوره، نوبت‌های رزروشده را لغو نمی‌کند. لغو نوبت عملی برگشت‌ناپذیر روی ظرفیت شعبه است و باید تصمیم صریح اپراتور باشد، نه اثر جانبی بستن یک دوره. نوبت‌ها را جداگانه لغو کنید.


چرخهٔ جلسه و اتصال به نوبت

اتفاق چه می‌شود
رزرو جلسه CourseSessionbooked و پیوند دوطرفه با نوبت
لغو نوبت همان جلسه → planned؛ بقیهٔ دوره دست‌نخورده
انجام جلسه completed + completed_at؛ دوره وقتی completed می‌شود که همهٔ جلساتش تمام شده باشند

پیوند دوطرفه است (course_sessions.appointment_id و appointments.course_session_id) تا لیست نوبت‌ها بدون JOIN بفهمد نوبت جزو دوره است و صفحهٔ دوره بدون JOIN نوبت را پیدا کند. هر دو ستون فقط در CourseSessionLinker نوشته می‌شوند.


طبقه‌بندی محیط

جدول وضعیت
course_protocols · treatment_courses · course_sessions جفت محیط
course_protocol_steps AGGREGATE_CHILDREN — ریشه CourseProtocol

تست‌ها

ddev exec php bin/phpunit tests/Course   # ۱۴ تست

مهم‌ترین‌ها: testChangingTheProtocolLeavesRunningCoursesAlone (snapshot)، testTheSuggestionAnchorsOnTheLastCompletedSession (لنگر متحرک) و testCancellingOneSessionOnlyResetsThatSession.