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>
164 lines
5.3 KiB
Markdown
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}
|
|
```
|