Appointments now persist address_id resolved from the weekly-schedule session (location_id) across all booking paths (online, secretary, admin). On confirm, the patient is added to the clinic owning that address, or to the doctor's single clinic as fallback. Weekly-schedule create/update now requires location_id on every active session. PatientSession exposes doctor_uuid/doctor_name so clinic records show which doctor each visit is for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
16 KiB
Appointment Settings API
Prefix:
/api/v1/appointment-settings
Permission: All write endpoints requireAUTH— must be the doctor owner orROLE_ADMIN
Doctors configure their availability via three resources: weekly schedule, date overrides, and holidays.
Weekly Schedule
Each doctor has one weekly schedule (upsert). The schedule is keyed by day index (0=Saturday ... 6=Friday), each day containing a sessions array.
Day Index Convention
| Index | Day (EN) | Day (FA) |
|---|---|---|
"0" |
Saturday | شنبه |
"1" |
Sunday | یکشنبه |
"2" |
Monday | دوشنبه |
"3" |
Tuesday | سهشنبه |
"4" |
Wednesday | چهارشنبه |
"5" |
Thursday | پنجشنبه |
"6" |
Friday | جمعه |
POST /api/v1/appointment-settings/weekly-schedule
Create or update the weekly schedule for a doctor (upsert).
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
الزام آدرس: هر session با
active=trueبایدlocation_id(آدرس مطب/کلینیک) داشته باشد. در غیر این صورت422 ERR_VALIDATION_001(«برای هر شیفت فعال باید آدرس انتخاب شود»). این آدرس هنگام رزرو خودکار روی نوبت ذخیره میشود.
Request Body (application/json)
{
"doctor_uuid": "550e8400-e29b-41d4-a716-446655440000",
"schedule": {
"0": {
"sessions": [
{
"active": true,
"location_id": 1973,
"start_time": "09:00",
"end_time": "13:00",
"duration_per_patient": 20,
"has_rest": true,
"rest_interval": 60,
"time_to_rest": 10,
"patient_limit": null
},
{
"active": true,
"location_id": 1973,
"start_time": "15:00",
"end_time": "18:00",
"duration_per_patient": 20,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": 5
}
]
},
"1": { "sessions": [] },
"2": { "sessions": [] },
"3": { "sessions": [] },
"4": { "sessions": [] },
"5": { "sessions": [] },
"6": { "sessions": [] }
}
}
| Field | Type | Required | Description |
|---|---|---|---|
doctor_uuid |
string (UUID) | ✅ | Doctor UUID |
schedule |
object | ✅ | Keys "0" through "6" (day indices) |
schedule.{n}.sessions |
array | ✅ | Array of session config objects |
meta |
object | ❌ | Online-booking settings (see below) |
Online-booking meta object:
{
"meta": {
"online_booking_enabled": true,
"booking_window_value": 2,
"booking_window_unit": "month"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
online_booking_enabled |
boolean | ❌ | false = no online booking; the public slot/month endpoints return no availability |
booking_window_value |
integer | ❌ | How far ahead patients may book (≥ 1) |
booking_window_unit |
string | ❌ | "week" or "month" |
Defaults when
metais absent:{ online_booking_enabled: true, booking_window_value: 1, booking_window_unit: "month" }.metais stored inside the schedulesettingJSON (no DB migration) and is preserved when onlyscheduleis sent.SlotCalculatorServicerejects any date in the past, beyondtoday + value unit, or when online booking is disabled — for the weekly schedule, date overrides, andappointment-slotsalike.
Session Config Object:
| Field | Type | Required | Description |
|---|---|---|---|
active |
boolean | ✅ | Whether this session is active |
location_id |
integer|null | ❌ | Doctor address/location ID |
start_time |
string | ✅ | Session start "HH:MM" |
end_time |
string | ✅ | Session end "HH:MM" |
duration_per_patient |
integer | ✅ | Minutes per appointment slot |
has_rest |
boolean | ❌ | Whether to insert rest breaks |
rest_interval |
integer | ❌ | Work minutes before taking a rest break |
time_to_rest |
integer | ❌ | Duration of each rest break (minutes) |
patient_limit |
integer|null | ❌ | Max patients per session (null = unlimited) |
Multiple sessions per day are supported (e.g., morning + afternoon). Sessions are sorted by
start_timeand overlapping ones are skipped.
Response 201
{
"success": true,
"data": {
"data": {
"uuid": "sched-uuid-...",
"doctor_uuid": "550e8400-...",
"schedule": {
"0": { "sessions": [ { "active": true, "start_time": "09:00", ... } ] },
"1": { "sessions": [] },
"2": { "sessions": [] },
"3": { "sessions": [] },
"4": { "sessions": [] },
"5": { "sessions": [] },
"6": { "sessions": [] }
},
"meta": {
"online_booking_enabled": true,
"booking_window_value": 1,
"booking_window_unit": "month"
},
"created_at": 1717000000,
"updated_at": 1717000000
}
}
}
⚠️ Double-nested: Frontend extracts with
data?.data?.data
This is an upsert — if a schedule already exists for the doctor, it is overwritten.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_VALIDATION_002 |
404 | Doctor not found |
ERR_AUTH_006 |
403 | Not the doctor owner |
GET /api/v1/appointment-settings/weekly-schedule/{uuid}
Get weekly schedule. {uuid} can be either the schedule UUID or the doctor UUID — the controller tries both.
Permission: AUTH (class-level IS_AUTHENTICATED_FULLY)
Response 200
Same structure as POST response.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_VALIDATION_002 |
404 | Schedule not found |
PATCH /api/v1/appointment-settings/weekly-schedule/{uuid}
Update weekly schedule. {uuid} can be schedule UUID or doctor UUID.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Request Body
{
"schedule": {
"0": {
"sessions": [
{
"active": true,
"location_id": 1973,
"start_time": "10:00",
"end_time": "14:00",
"duration_per_patient": 30,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
]
}
}
}
Replaces the entire
scheduleobject if provided.
Response 200
Updated schedule object (same structure as POST).
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner |
ERR_VALIDATION_002 |
404 | Schedule not found |
DELETE /api/v1/booking-setting/{uuid}
Delete a weekly schedule.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Note: route is
/booking-setting/, not/appointment-settings/
Response 200
{ "success": true, "data": { "message": "برنامه هفتگی با موفقیت حذف شد" } }
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner |
ERR_VALIDATION_002 |
404 | Schedule not found |
Date Overrides
Override a specific date — mark it inactive (day off) or give it custom sessions.
GET /api/v1/appointment-settings/date-override/list/{doctorUuid}
Get all date overrides for a doctor.
Permission: AUTH (class-level)
Response 200
{
"success": true,
"data": {
"data": [
{
"uuid": "...",
"doctor_uuid": "...",
"date": 1718476800,
"active": false,
"reason": "تعطیل خاص",
"custom_slots": [],
"created_at": 1717000000
}
]
}
}
dateis a Unix timestamp.active: false= entire day blocked.active: truewithcustom_slots= custom session schedule.
POST /api/v1/appointment-settings/date-override
Create a date override.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Request Body
{
"doctor_uuid": "550e8400-...",
"date": "2024-06-20",
"active": false,
"reason": "تعطیل رسمی",
"custom_slots": null
}
| Field | Type | Required | Description |
|---|---|---|---|
doctor_uuid |
string (UUID) | ✅ | Doctor UUID |
date |
string | ✅ | Date in Y-m-d format (e.g. "2024-06-20") |
active |
boolean | ❌ | false = full day off (default); true = use custom_slots |
reason |
string | ❌ | Reason for override |
custom_slots |
array|null | ❌ | Custom sessions (same SessionConfig format as weekly schedule, see below) |
custom_slots format when active: true:
{
"custom_slots": [
{
"start_time": "14:00",
"end_time": "18:00",
"duration_per_patient": 20,
"location_id": 1973,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
]
}
Legacy format (backward compatible):
{
"custom_slots": [
{ "start": "14:00", "end": "18:00", "duration": 20 }
]
}
Response 201
{
"success": true,
"data": {
"data": {
"uuid": "override-uuid-...",
"doctor_uuid": "...",
"date": 1718476800,
"active": false,
"reason": "تعطیل رسمی",
"custom_slots": [],
"created_at": 1717000000
}
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner |
ERR_VALIDATION_002 |
404 | Doctor not found |
ERR_VALIDATION_001 |
422 | Invalid date format |
GET /api/v1/appointment-settings/date-override/{uuid}
Get a single date override.
Permission: AUTH (class-level)
Response 200
Override object (same structure as above).
PATCH /api/v1/appointment-settings/date-override/{uuid}
Update a date override.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Request Body (all optional)
{
"active": true,
"reason": "جبران مرخصی",
"custom_slots": [
{
"start_time": "14:00",
"end_time": "18:00",
"duration_per_patient": 20,
"location_id": 1973,
"has_rest": false,
"rest_interval": 0,
"time_to_rest": 0,
"patient_limit": null
}
],
"date": "2024-06-21"
}
Response 200
Updated override object.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner |
ERR_VALIDATION_002 |
404 | Override not found |
DELETE /api/v1/appointment-settings/date-override/{uuid}
Delete a date override.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Response 200
{ "success": true, "data": { "message": "Override با موفقیت حذف شد" } }
Holidays
Mark a date range as holiday — all slots blocked, no overrides apply.
GET /api/v1/appointment-settings/holidays/list/{doctorUuid}
Get all holidays for a doctor.
Permission: AUTH (class-level)
Response 200
{
"success": true,
"data": {
"data": [
{
"uuid": "...",
"doctor_uuid": "...",
"start_date": 1719792000,
"end_date": 1720656000,
"reason": "تعطیلات تابستانی",
"active": true,
"created_at": 1717000000
}
]
}
}
start_dateandend_dateare Unix timestamps. Holidays take highest priority — they block the day even if a date override exists.
POST /api/v1/appointment-settings/holidays
Create a holiday range.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Request Body
{
"doctor_uuid": "550e8400-...",
"start_date": "2024-07-01",
"end_date": "2024-07-10",
"reason": "تعطیلات تابستانی"
}
| Field | Type | Required | Description |
|---|---|---|---|
doctor_uuid |
string (UUID) | ✅ | Doctor UUID |
start_date |
string | ✅ | Start date Y-m-d |
end_date |
string | ✅ | End date Y-m-d (must be ≥ start_date) |
reason |
string | ❌ | Holiday reason |
Response 201
{
"success": true,
"data": {
"data": {
"uuid": "holiday-uuid-...",
"doctor_uuid": "...",
"start_date": 1719792000,
"end_date": 1720656000,
"reason": "تعطیلات تابستانی",
"active": true,
"created_at": 1717000000
}
}
}
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_AUTH_001 |
401 | Missing token |
ERR_AUTH_006 |
403 | Not the doctor owner |
ERR_VALIDATION_002 |
404 | Doctor not found |
ERR_VALIDATION_001 |
422 | end_date before start_date or invalid format |
GET /api/v1/appointment-settings/holidays/{uuid}
Get a single holiday.
Permission: AUTH (class-level)
PATCH /api/v1/appointment-settings/holidays/{uuid}
Update a holiday.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Request Body (all optional)
{
"start_date": "2024-07-02",
"end_date": "2024-07-12",
"reason": "تمدید تعطیلات",
"active": false
}
Response 200
Updated holiday object.
DELETE /api/v1/appointment-settings/holidays/{uuid}
Delete a holiday.
Permission: AUTH — must be the doctor owner or ROLE_ADMIN
Response 200
{ "success": true, "data": { "message": "تعطیلات حذف شد" } }
Slot Calculation Logic (Reference)
The SlotCalculatorService calculates available slots in this priority order:
- Holiday — if date falls in a holiday range → return empty (no slots)
- Date Override — if a date override exists for this date:
active: false→ return emptyactive: true→ usecustom_slotssessions
- Weekly Schedule — use the day's
sessionsarray (onlyactive: truesessions, sorted bystart_time, overlapping sessions skipped)
Slot output format:
[
{
"start": 1718438400,
"end": 1718439600,
"start_time": "09:00",
"end_time": "09:20",
"location_id": 1973
}
]
Available Locations
GET /api/v1/appointment-settings/available-locations/{doctorUuid}
Permission: Public
Returns all locations a doctor can assign as location_id in their schedule sessions. Includes both the doctor's personal addresses and the addresses of all clinics they belong to.
Response 200
{
"success": true,
"data": [
{
"id": "5",
"uuid": "...",
"type": "personal",
"clinic_id": null,
"clinic_name": null,
"name": "مطب شخصی",
"address": "تهران، ...",
"telephone": "09121234567",
"map": { "latitude": "35.7", "longitude": "51.4" },
"city": { "id": "1", "name": "تهران" },
"province": { "id": "8", "name": "تهران" }
},
{
"id": "12",
"uuid": "...",
"type": "clinic",
"clinic_id": "3",
"clinic_name": "کلینیک نور",
"name": "شعبه مرکزی",
"address": "تهران، خیابان ولیعصر...",
"telephone": "02112345678",
"map": { "latitude": "35.699", "longitude": "51.337" },
"city": { "id": "1", "name": "تهران" },
"province": { "id": "8", "name": "تهران" }
}
]
}
| Field | Description |
|---|---|
type |
personal = doctor's own address; clinic = clinic address |
clinic_id |
ID of the clinic (only for type=clinic) |
clinic_name |
Name of the clinic (only for type=clinic) |
Use
idas thelocation_idvalue in weekly schedule sessions or date override sessions.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_002 |
404 | Doctor not found |