Files
clinicpro/docs/api/treatment.md
T
hamedandClaude Opus 5 252e20bfe9 feat(treatment): select treatment behaviour by practice domain, not by if-branch
Everything that differs between specialties as data is already stored as data.
What is left is behaviour — when a case opens, what happens once a session ends —
so it becomes a TreatmentWorkflow resolved through a tagged-service registry.
The booking path calls one collaborator and never names a specialty; adding
dentistry is a new class, not an edit to confirmation.

A clinic that has chosen no practice domain still gets working multi-session
courses: DefaultTreatmentWorkflow answers for null and for any code without a
dedicated implementation, keeping "unset means behave as today, not error".
LaserTreatmentWorkflow is deliberately empty beyond claiming `beauty` — it is the
seam where laser-specific behaviour will land without disturbing anyone else.

Session due dates are anchored to the previous session's actual finish, so a
patient who comes twenty days late shifts the rest of their course instead of
getting the next session while it can still do nothing. Only the next session is
recomputed; later ones keep their estimate because they are anchored to nothing
yet.

Attachment targets the first session without an appointment rather than the
first open one: a patient booking again mid-course was otherwise matched to the
session that already had a booking, and the second appointment went nowhere.

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

8.8 KiB
Raw Blame History

Treatment API

Prefix: /api/v1/service-item/{uuid}/treatment-protocol

A treatment protocol is the "طول درمان" of a service: it says a course of that service runs over several sessions, when each one falls due, which doctor supervises it, and which staff may perform it. A service without a protocol row is single-session — the row's existence is the switch, which is why there is no separate boolean.

ServiceItem.session_count is deprecated. It never had logic behind it; session count now comes from the protocol's step list. The column still appears in service payloads so existing clients do not break, but nothing should read it.

Superseded design note: an earlier src/Course/ module (CourseProtocol / TreatmentCourse) was deleted in 65d5831c and its docs removed. It modelled the gap between sessions as one min/ideal/max triple for the whole course, which cannot express a course whose intervals differ per session — a botox course is session 1, then +15 days, then monthly. This design stores an explicit offset per step instead.


The step model

Each step carries offset_days, and that offset is measured from the previous session, not from the start of the course. The spacing of a laser course is a clinical requirement — hair regrows relative to the last treatment, not relative to when the file was opened — so a patient who arrives late shifts the rest of their course rather than getting the next session too early.

Rule چرا
حداقل ۲ گام یک گام یعنی سرویس تک‌جلسه‌ای؛ سوییچ اصلاً نباید روشن باشد
حداکثر ۶۰ گام سقف عقلانی، جلوی ورودی اشتباه را می‌گیرد
step_number پیوسته از ۱ «جلسهٔ ۳ از ۸» فقط وقتی معنی دارد که گامی جا نیفتاده باشد
گام ۱offset_days = 0 لنگر دوره است و «جلسهٔ قبل» ندارد
گام‌های بعدی → offset_days > 0 فاصلهٔ صفر یعنی دو جلسه در یک روز
حداقل یک پرسنل مجاز بدون آن هر پرسنلی پای هر دستگاهی می‌نشیند

GET /api/v1/service-item/{uuid}/treatment-protocol

Permission: IS_AUTHENTICATED_FULLY, محدود به محیط جاری — سرویس محیط دیگر 404 می‌گیرد.

data: null یعنی سوییچ خاموش است، نه اینکه چیزی پیدا نشد.

Response 200

{
    "success": true,
    "data": {
        "uuid": "8de51c47-ddd1-44ea-bae1-83cf6457b182",
        "service_uuid": "cc0b11ec-1c39-45f0-bc1d-5514619bc74a",
        "active": true,
        "total_sessions": 4,
        "supervisor": null,
        "steps": [
            { "step_number": 1, "offset_days": 0 },
            { "step_number": 2, "offset_days": 15 },
            { "step_number": 3, "offset_days": 30 },
            { "step_number": 4, "offset_days": 30 }
        ],
        "staff": [
            { "uuid": "53acc523-48a7-4f4a-89b1-745e8e7a69bd", "name": "پرسنل۱" }
        ]
    }
}

Errors:

Code HTTP توضیح
ERR_NOT_FOUND_001 404 سرویس یافت نشد یا مال محیط دیگری است
ERR_AUTH_001 401 بدون توکن

PUT /api/v1/service-item/{uuid}/treatment-protocol

Replace the whole protocol. Creates it on first call, so this doubles as "turn the switch on".

Everything is validated before anything is written: an invalid step at the end of the list must not wipe the valid steps already stored. Steps and staff are then cleared and rewritten inside one transaction.

Permission: IS_AUTHENTICATED_FULLY, محدود به محیط جاری.

Request Body (application/json)

Field Type Required توضیح
steps array ۲ تا ۶۰ گام
steps[].step_number int پیوسته از ۱؛ نبودنش یعنی ترتیب آرایه
steps[].offset_days int فاصله از جلسهٔ قبلی
staff_uuids string[] حداقل یکی، همه از محیط جاری و فعال؛ تکراری‌ها حذف می‌شوند
supervisor_doctor_uuid string پزشک ناظر؛ باید عضو همین محیط باشد
{
  "staff_uuids": ["53acc523-48a7-4f4a-89b1-745e8e7a69bd"],
  "supervisor_doctor_uuid": null,
  "steps": [
    { "step_number": 1, "offset_days": 0 },
    { "step_number": 2, "offset_days": 15 },
    { "step_number": 3, "offset_days": 30 },
    { "step_number": 4, "offset_days": 30 }
  ]
}

Response 200

Same shape as GET.

Errors:

Code HTTP Field توضیح
ERR_VALIDATION_001 422 steps کمتر از ۲ یا بیشتر از ۶۰ گام
ERR_VALIDATION_002 422 steps فیلد steps نیست یا آرایه نیست
ERR_VALIDATION_002 422 offset_days offset_days یک گام نیست یا عدد نیست
ERR_VALIDATION_001 422 offset_days گام اول صفر نیست، یا گام بعدی صفر/منفی است
ERR_VALIDATION_001 422 step_number شماره‌ها پیوسته از ۱ نیستند
ERR_VALIDATION_002 422 staff_uuids فهرست خالی است یا uuid نامعتبر دارد
ERR_NOT_FOUND_001 404 staff_uuids پرسنل یافت نشد، غیرفعال است، یا مال محیط دیگری است
ERR_NOT_FOUND_001 404 پزشک ناظر عضو این محیط نیست

Real 422 responses:

{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"دورهٔ درمان حداقل 2 جلسه دارد؛ کمتر از آن یعنی سرویس تک‌جلسه‌ای","field":"steps"}]}
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_002","message":"حداقل یک پرسنل مجاز الزامی است","field":"staff_uuids"}]}
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"شمارهٔ گام‌ها باید پیوسته از ۱ باشد؛ گام 2 انتظار می‌رفت","field":"step_number"}]}

Opening a case — what happens on confirm

There is no endpoint that opens a treatment case; it happens as a side effect of confirming an appointment, in AppointmentConfirmationService::onConfirmed:

نوبت تأیید شد
  → PatientSession ساخته می‌شود (مالی، مثل همیشه)
  → اگر سرویسِ نوبت پروتکل فعال دارد:
       TreatmentWorkflowRegistry::for(clinic.practice_domain.code)->openCase(...)

The booking core never names a specialty. A TreatmentWorkflow is selected by the clinic's practice domain code through a tagged-service registry, so adding dentistry is a new class rather than a change in the booking path. LaserTreatmentWorkflow handles beauty; DefaultTreatmentWorkflow answers for everything else, including a clinic that has chosen no domain at all — null means "behave as today", never an error.

What opening a case does:

پروندهٔ باز موجود برگردانده می‌شود؛ پروندهٔ دوم برای همان بیمار و همان سرویس ساخته نمی‌شود
نواحی برگ‌های دستهٔ سرویس، با نامشان، در همان لحظه کپی می‌شوند
جلسات همهٔ گام‌های پروتکل ساخته می‌شوند، همه planned
نوبت به اولین جلسهٔ بدون نوبت می‌چسبد و آن جلسه booked می‌شود
سررسید جلسهٔ رزروشده ساعت نوبت را می‌گیرد؛ بقیه null می‌مانند

Failure to open a case is logged and swallowed — the appointment is booked and possibly paid for, and losing that is worse than losing the case file, which can be rebuilt.

Session due dates

due_at of session n is finished_at of session n1 plus that step's offset_days. Only the next session is recomputed when one finishes; sessions further out keep their earlier estimate, because a number that is not yet anchored to anything real does not get more accurate by being recalculated.

A no-show does not burn the session: its status becomes no_show, its appointment link is cleared, total_sessions is untouched, and the same session comes back to the front of the booking queue.


DELETE /api/v1/service-item/{uuid}/treatment-protocol

Turn the switch off — the protocol, its steps and its staff list are removed and the service is single-session again. Idempotent: deleting a service that has no protocol still answers 200.

Permission: IS_AUTHENTICATED_FULLY, محدود به محیط جاری.

Response 200

{ "success": true, "data": null }