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>
5.3 KiB
Practice Domain API
Prefix:
/api/v1/practice-domains,/api/v1/practice-domain
A practice domain is the field a clinic operates in — beauty, dentistry, orthopaedics. It is a
configuration key, not a marketing label: treatment workflows bind to its code, so the code is
immutable once created. It is deliberately not Specialty, which stays a descriptive label for
the public booking site.
The table is global (registered in GlobalTables::ENTITIES); a clinic points at zero or one of its
rows through clinics.practice_domain_id, which is nullable. NULL means "not configured" and keeps
today's behaviour — it is never an error.
GET /api/v1/practice-domains
List domains, ordered by sort_order then name.
Permission: IS_AUTHENTICATED_FULLY. Callers without ROLE_ADMIN see only active rows, so a
clinic manager cannot pick a domain the platform has retired. ROLE_ADMIN sees inactive ones too, to
be able to switch them back on.
Response 200
{
"success": true,
"data": [
{
"uuid": "8c7bfd18-9159-11f1-b98b-f28fd8aa5db5",
"code": "beauty",
"name": "کلینیک زیبایی",
"sort_order": 0,
"active": true,
"has_workflow": true
}
]
}
has_workflow میگوید آیا پیادهسازیِ TreatmentWorkflow مخصوصِ این کد ثبت شده یا حوزه
فقط رفتار پیشفرض میگیرد. حوزهٔ بیworkflow کار میکند — دورههای چندجلسهایاش باز
میشوند — ولی رفتار اختصاصی ندارد، و پنل ادمین پلتفرم باید همین را نشان دهد نه اینکه
مدیر حدس بزند.
Errors:
| Code | HTTP | توضیح |
|---|---|---|
| ERR_AUTH_001 | 401 | بدون توکن |
POST /api/v1/practice-domains
Create a domain.
Permission: ROLE_ADMIN (platform admin only). A clinic manager creating its own domain would
produce a domain with no workflow behind it, and the misconfiguration would stay invisible until the
first protocol-driven booking.
Request Body (application/json)
| Field | Type | Required | توضیح |
|---|---|---|---|
code |
string | ✅ | ^[a-z0-9_]{1,40}$، یکتا در کل جدول، بعد از ساخت تغییرناپذیر |
name |
string | ✅ | نام نمایشی فارسی |
sort_order |
int | ❌ | پیشفرض 0 |
{ "code": "dental", "name": "دندانپزشکی", "sort_order": 2 }
Response 201
{
"success": true,
"data": {
"uuid": "af5063de-753f-444a-ba95-05ec2ffe13be",
"code": "dental",
"name": "دندانپزشکی",
"sort_order": 2,
"active": true
}
}
Errors:
| Code | HTTP | توضیح |
|---|---|---|
| ERR_VALIDATION_001 | 422 | بدنهٔ نامعتبر، یا کد خارج از الگو، یا کد تکراری (field: code) |
| ERR_VALIDATION_002 | 422 | name خالی است (field: name) |
| ERR_FORBIDDEN_001 | 403 | کاربر ROLE_ADMIN نیست |
| ERR_AUTH_001 | 401 | بدون توکن |
Real 422 for a duplicate code:
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_001","message":"حوزه فعالیتی با این کد از قبل وجود دارد","field":"code"}]}
PATCH /api/v1/practice-domain/{uuid}
Update a domain's display fields.
Permission: ROLE_ADMIN.
code is ignored if sent. Workflow implementations are resolved by code, so renaming it would
silently detach a live clinic from its workflow.
Request Body (application/json)
| Field | Type | توضیح |
|---|---|---|
name |
string | فقط اگر ناتهی باشد اعمال میشود |
sort_order |
int | |
active |
bool |
Response 200
{
"success": true,
"data": {
"uuid": "af5063de-753f-444a-ba95-05ec2ffe13be",
"code": "dental",
"name": "دندانپزشکی",
"sort_order": 5,
"active": true
}
}
Errors:
| Code | HTTP | توضیح |
|---|---|---|
| ERR_VALIDATION_001 | 422 | بدنهٔ درخواست نامعتبر است |
| ERR_VALIDATION_002 | 404 | حوزه فعالیت یافت نشد |
| ERR_FORBIDDEN_001 | 403 | کاربر ROLE_ADMIN نیست |
Assigning a domain to a clinic
There is no dedicated endpoint. The existing PATCH /api/v1/clinic/{uuid} accepts one more key —
see clinic.md.
| Body | اثر |
|---|---|
"practice_domain_uuid": "<uuid>" |
حوزه ست میشود |
"practice_domain_uuid": "" یا null |
حوزه پاک میشود |
| کلید اصلاً نباشد | حوزهٔ فعلی دستنخورده میماند |
An unknown uuid is rejected rather than ignored, because a silently dropped selection would only surface at the first protocol-driven booking:
{"success":false,"data":null,"errors":[{"code":"ERR_VALIDATION_002","message":"حوزه فعالیت یافت نشد","field":"practice_domain_uuid"}]}
GET /api/v1/clinic/{uuid} returns the current value under data.data.practice_domain, null when
unset:
{"uuid":"8c7bfd18-9159-11f1-b98b-f28fd8aa5db5","code":"beauty","name":"کلینیک زیبایی","sort_order":0,"active":true}