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>
This commit is contained in:
hamed
2026-08-06 17:23:51 +03:30
co-authored by Claude Opus 5
parent 9af763bfbe
commit 252e20bfe9
14 changed files with 892 additions and 5 deletions
+7 -1
View File
@@ -31,12 +31,18 @@ be able to switch them back on.
"code": "beauty",
"name": "کلینیک زیبایی",
"sort_order": 0,
"active": true
"active": true,
"has_workflow": true
}
]
}
```
`has_workflow` می‌گوید آیا پیاده‌سازیِ `TreatmentWorkflow` مخصوصِ این کد ثبت شده یا حوزه
فقط رفتار پیش‌فرض می‌گیرد. حوزهٔ بی‌workflow کار می‌کند — دوره‌های چندجلسه‌ای‌اش باز
می‌شوند — ولی رفتار اختصاصی ندارد، و پنل ادمین پلتفرم باید همین را نشان دهد نه اینکه
مدیر حدس بزند.
**Errors:**
| Code | HTTP | توضیح |
|------|------|-------|
+43
View File
@@ -130,6 +130,49 @@ Real 422 responses:
---
## 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