fix(booking): carry the clinic context through the panel and drop phantom locations
Two faults, one root: the per-context booking work updated ScheduleSection but left the rest of the panel calling slot endpoints without clinic_uuid. Absent clinic_uuid means the personal practice, so the panel asked about a schedule the doctor barely uses and got nothing back. - useClinicContext() resolves the current environment once and is used by the appointments page, useDoctorBookingServices, ServiceSlotPicker and both queries in NewAppointmentDrawer (a fifth call site a sweep turned up). It returns null in a doctor's personal environment so the mirror-image bug — a doctor seeing the clinic's schedule at their own practice — cannot appear. clinicUuid is part of every query key; without it the cache leaks across environments. - appointment-slots returns empty_reason (no_schedule | holiday | day_off | outside_window). TurnsTimeline rendered «این روز تعطیل است» for any empty day, which is what the bug report actually saw; it now says which of the four it is. - booking-locations lists a location only when the context has an address and an active shift points at it. The dev data had three "personal" schedules whose shifts referenced the clinic's address, so the public site advertised a personal practice that could never be booked. - ?date= adds available_on_date per location, validated as a real calendar date. - MyAppointmentsController and AdminApiController resolved the appointment address with no context and could store the wrong one. Both now go through the new BookingContextResolver, which also replaces AppointmentController's private copy of the same membership check. - app:schedule:audit-locations reports shifts pointing at a missing or foreign address; --fix deactivates them rather than deleting. Verified against the reported doctor: same date, no clinic_uuid -> 0 sessions, with it -> 1 session; a full week matches the configured Sat/Tue/Wed/Thu. Suite: 417 tests, 2 failures — both pre-existing and unrelated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+46
-1
@@ -701,6 +701,8 @@ Every public booking endpoint therefore accepts an optional **`clinic_uuid`**:
|
||||
| `GET /api/v1/appointment-booking-services/{doctorUuid}` | query |
|
||||
| `GET /api/v1/appointment-settings/month-availability/{doctorUuid}` | query |
|
||||
| `POST /api/v1/appointment` | body |
|
||||
| `POST /api/v1/my/appointment` | body |
|
||||
| admin booking (`src/Admin/`) | body |
|
||||
|
||||
Omitting it means the **personal practice** — it is never a wildcard. If the doctor is not a member
|
||||
of the given clinic → `404 ERR_VALIDATION_002` («محل نوبتدهی یافت نشد»). All four `GET`s echo back
|
||||
@@ -771,7 +773,8 @@ location — picking one and hiding the rest removes real capacity from the doct
|
||||
| `location_uuid` | `string\|null` | the `DoctorAddress` uuid; `null` when the context has no address yet |
|
||||
| `type` | `"personal" \| "clinic"` | |
|
||||
| `booking_mode` | `"slot" \| "service"` | per-context — the same doctor can differ between locations |
|
||||
| `opening_hours` | `array` | active weekly shifts of that context, flattened; `day` is the English weekday name so it maps straight onto schema.org `openingHoursSpecification` |
|
||||
| `opening_hours` | `array` | active weekly shifts of that context, flattened; each entry is `{day, day_index, location_id, opens, closes}`. `day` is the English weekday name so it maps straight onto schema.org `openingHoursSpecification`; `day_index` is the schedule key (0=Saturday) |
|
||||
| `available_on_date` | `bool\|null` | only when `?date=` is supplied — whether that location has a free slot that day. `null` without `date` |
|
||||
| `services` | `array` | populated only in `service` mode, scoped to that context's owner |
|
||||
| `next_available_at` | `int\|null` | Unix timestamp of the earliest free slot within 30 days, capped by the context's booking window |
|
||||
|
||||
@@ -786,6 +789,29 @@ selection; locations with no capacity sort last. Deep links should carry the cho
|
||||
|
||||
**Status codes:** `200`, `404 ERR_VALIDATION_002` (doctor not found).
|
||||
|
||||
#### A location must be bookable to be listed
|
||||
|
||||
An entry is returned only when **both** hold:
|
||||
|
||||
1. the context has at least one registered address, **and**
|
||||
2. at least one active shift points at one of those addresses.
|
||||
|
||||
A schedule whose shifts carry no `location_id`, or point at an address belonging to a different
|
||||
context (a personal schedule referencing a clinic address, say), is not a place a patient can go —
|
||||
it is omitted entirely, and its shifts never appear in `opening_hours`.
|
||||
|
||||
This filters out rows that predate the `validateSessions` rule, which now rejects such shifts at
|
||||
write time. Use `php bin/console app:schedule:audit-locations` to list the offenders; `--fix`
|
||||
deactivates them (it never deletes — the hours are user data). If the schedule was in truth a
|
||||
clinic's, move it instead with `app:schedule:assign-clinic`.
|
||||
|
||||
#### `?date=YYYY-MM-DD`
|
||||
|
||||
Adds `available_on_date` to every entry and echoes `date` in the response. Use it to grey out
|
||||
locations that cannot be booked on the day the patient picked, rather than showing an empty slot
|
||||
list. An impossible date (`2026-13-99`) → `422 ERR_VALIDATION_001` on field `date`; the check is a
|
||||
real calendar check, not just a regex.
|
||||
|
||||
`opening_hours` lists one entry per active shift, so a day with a morning and an evening shift
|
||||
appears twice. Days with no active shift are absent. Times are local `HH:MM` strings, and the
|
||||
per-shift `location_id` is not repeated here — every shift in an entry already belongs to that
|
||||
@@ -794,3 +820,22 @@ location's context.
|
||||
**Consumer:** `nobat724_front` — the doctor page must render one booking block per entry, pass the
|
||||
matching `clinic_uuid` into the slot and booking calls, and feed `opening_hours` into the
|
||||
`openingHoursSpecification` of each `MedicalClinic` in the Physician JSON-LD.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Why a day has no slots — `empty_reason` (2026-07)
|
||||
|
||||
`GET /api/v1/appointment-slots` now returns `empty_reason` alongside `sessions`. It is `null` when
|
||||
sessions exist, otherwise one of:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `no_schedule` | no weekly schedule exists for that **context** — the commonest cause is a caller that forgot `clinic_uuid` and so asked about the personal practice |
|
||||
| `holiday` | an active holiday covers the day (the doctor's own global holiday, or one the clinic set) |
|
||||
| `day_off` | a schedule exists but that weekday has no active shift |
|
||||
| `outside_window` | the date is past, beyond the booking window, or online booking is switched off |
|
||||
|
||||
Clients must not translate an empty `sessions` array into "closed". The admin panel used to do
|
||||
exactly that and reported «این روز تعطیل است» for a doctor whose clinic schedule was perfectly
|
||||
active — the request simply carried no `clinic_uuid`.
|
||||
|
||||
Reference in New Issue
Block a user