Files
clinicpro/docs/api/practice-domain.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

164 lines
5.3 KiB
Markdown

# 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`
```json
{
"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` |
```json
{ "code": "dental", "name": "دندانپزشکی", "sort_order": 2 }
```
### Response `201`
```json
{
"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:
```json
{"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`
```json
{
"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](./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:
```json
{"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:
```json
{"uuid":"8c7bfd18-9159-11f1-b98b-f28fd8aa5db5","code":"beauty","name":"کلینیک زیبایی","sort_order":0,"active":true}
```