Files
clinicpro/docs/api/course.md
T
hamedandClaude Opus 5 fc504f4415 feat(course): treatment courses with protocol-driven session planning
Laser is six to eight sessions; the previous design only knew single
appointments, which is the exception rather than the rule.

- CourseProtocol per service: session count and three distinct spacings —
  min is the earliest that is clinically allowed, ideal is best, max is where
  the course starts losing its effect
- Starting a course creates every session up front as `planned` and copies the
  protocol's numbers and per-session params, so changing the protocol tomorrow
  leaves a running course alone
- Suggestions anchor on the last *completed* session, not the course start:
  when session 2 slips, session 3 moves with it
- Slots are ranked by distance from ideal, not by earliest available — day 21
  is worse than day 27 when 28 is the target
- book-all is all-or-nothing inside one transaction, with a moving anchor and a
  90-day horizon; sessions past the horizon stay planned and are reported, not
  treated as failures
- The effective minimum is the stricter of the protocol and the task-09 spacing
  policy, so a clinic rule never fights the protocol
- Cancelling one session returns only that session to planned; abandoning a
  course does not cancel its appointments, which stays an explicit decision

One active course per (patient, service) via active_course_key, the same
partial-uniqueness trick as Appointment::activeSlotKey.

Admin: CourseProtocolsPage, TreatmentCoursePage and a courses tab on the
patient record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 11:33:07 +03:30

11 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,
    "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": {}, "…": "جلسه‌ای که پروتکل برایش پارامتر ندارد" }
    ]
  }
}

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.