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

187 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`
```json
{
"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 | ❌ | پزشک ناظر؛ باید عضو همین محیط باشد |
```json
{
"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:
```json
{"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`
```json
{ "success": true, "data": null }
```