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>
8.8 KiB
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 n−1 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 }