feat(appointments): backend for clinic workflow (Figma نوبت‌ها) — phase A

Extend Appointment for the clinic-facing appointments area:

- New nullable relations service_section/service_item/staff (بخش/سرویس/پرسنل)
  plus deposit_required/deposit_amount_rials (بیعانه) and is_reserve.
- New statuses following_up (در حال پیگیری) and salon (سالن) with day-of
  transition rules; reserve entries never occupy a slot (several reserves may
  share one day), enforced in refreshActiveSlotKey.
- rescheduleTo(slotStart, slotEnd, isReserve) keeps active_slot_key consistent
  for جا به جایی and reserve transfers.
- New PATCH /api/v1/appointment/{uuid}: partial update covering edit, slot
  move (409 on taken slot, race backstop on the unique key), reserve toggle,
  patient swap (جایگزینی) and optional status transition; optimistic lock via
  version like the status endpoint.
- POST /my/appointment now accepts the workflow fields and is_reserve
  (day-level entry: no past-slot rule, no atomic slot booking); GET
  /my/appointments gains reserve=1 and returns the new fields per row.

Migration Version20260713195434 (+ mirrored on db_test). Docs updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-07-13 23:37:32 +03:30
co-authored by Claude Fable 5
parent 8287a48af1
commit 4678739d15
8 changed files with 671 additions and 11 deletions
+44
View File
@@ -475,3 +475,47 @@ Role-aware paginated list of appointments. Returns only what the authenticated u
"currentPage": 1
}
}
---
## Clinic workflow extensions (نوبتها Figma)
New optional fields on `Appointment` (all backward-compatible): `service_section` (بخش), `service_item` (سرویس), `staff` (پرسنل), `deposit_required` / `deposit_amount_rials` (بیعانه), `is_reserve` (نوبت رزرو day-level, never occupies a slot).
New statuses: `following_up` (در حال پیگیری), `salon` (سالن). Transitions:
`pending confirmed|following_up|cancelled_*|expired` · `confirmed completed|following_up|salon|cancelled_*|no_show` · `following_up confirmed|salon|completed|cancelled_*|no_show` · `salon completed|following_up|cancelled_*|no_show`
### PATCH `/api/v1/appointment/{uuid}`
General update (ویرایش / جا به جایی / انتقال به رزرو / جایگزینی). All body fields optional; only present keys change. **Permission:** appointment's patient, owning doctor, or admin.
```json
{
"slot_start": 1731000000, "slot_end": 1731001800,
"is_reserve": false,
"service_section_uuid": "…", "service_item_uuid": "…", "staff_uuid": "…",
"deposit_required": true, "deposit_amount_rials": 5000000,
"note": "…", "patient_name": "…", "patient_mobile": "…",
"status": "confirmed", "version": 3
}
```
- `slot_start`/`slot_end` must be sent together; moving to an occupied slot → `409`.
- Relation uuids: empty string clears; unknown uuid → `422`.
- `status` follows the same transition rules as `PATCH /appointment/{uuid}/status`.
- Optimistic lock via `version``409` on concurrent edit.
Response `200`: `{ success, data: { data: <appointment.toArray()> } }`
| HTTP | Description |
|------|-------------|
| 404 | نوبت یافت نشد |
| 403 | not patient/doctor/admin |
| 422 | half slot pair, end < start, unknown relation uuid, invalid transition |
| 409 | slot taken or version conflict |
### POST `/api/v1/my/appointment` (extended)
Extra optional body fields: `service_section_uuid`, `service_item_uuid`, `staff_uuid`, `deposit_required`, `deposit_amount_rials`, `is_reserve`.
`is_reserve: true` → day-level reserve entry: `slot_end` may equal `slot_start`, the past-slot rule is skipped, and the entry never occupies a slot (several reserves may share a day). Response `201` now also returns `is_reserve`.
### GET `/api/v1/my/appointments` (extended)
New query param `reserve=1` → returns only reserve-list entries; without it only regular slot bookings are returned. Each row now also includes: `patient_uuid`, `is_reserve`, `deposit_required`, `deposit_amount_rials`, `note`, `service_section`, `service_item`, `staff` (each `{uuid, name|full_name}` or null).