A protocol says a course of a service runs over several sessions, when each
falls due, which doctor supervises it and which staff may perform it. The row
existing IS the "طول درمان" switch, so there is no separate boolean that could
disagree with the step list.
Each step's offset is measured from the previous session rather than from the
start of the course: laser spacing is a clinical requirement — hair regrows
relative to the last treatment — so a late patient shifts the rest of their
course instead of getting the next session early. That also lets one course use
uneven gaps, which a single min/ideal/max triple cannot express: a botox course
is session 1, then +15 days, then monthly.
Steps and staff are cleared and rewritten in two flushes inside a transaction.
A single flush sends inserts before deletes and the replacement row collides
with the unique (protocol, step_number) index — caught by the replace test.
Removes docs/api/course.md and the task-12 folder. They documented src/Course/,
a module deleted in 65d5831c whose commit message only mentions removing two
test files; that design is superseded by this one.
ServiceItem::$sessionCount is marked deprecated. It never had logic behind it
and session count now comes from the protocol; the column stays in payloads so
existing clients keep working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
144 lines
6.5 KiB
Markdown
144 lines
6.5 KiB
Markdown
# 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"}]}
|
||
```
|
||
|
||
---
|
||
|
||
## 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 }
|
||
```
|