feat(treatment): bind an appointment to a chosen session, and free it on cancel

Two holes in how a course's later appointments were made.

The link from the unbooked queue carried nothing — `/admin/appointments/new`
with no parameters — so the secretary retyped the patient and the service, and
which case the appointment joined was inferred from the service they happened to
pick. A patient with two open courses had no way to say which one they meant,
and picking the wrong service silently opened a third case. (The suggestion link
did pass slot_start and resource_uuid, but the create page never read either.)

POST /api/v1/my/appointment now takes an optional treatment_session_uuid.
SessionBookingLink validates it — same tenant, still unbooked, case open, same
patient — and reserves that session. Confirm-time attachment steps aside when
the appointment already holds a session. The booking form states in words which
session, which course and which patient it is about to book, read from a new
GET /api/v1/treatment-session/{uuid}.

Nothing ever detached a session from its appointment, so a cancelled booking
left the session `booked` forever, and since findNextUnbooked requires
"has no appointment", it could never return to the queue. Cancellation and
no-show now release it back to `planned`. A finished session is history and is
left alone.

The system still never books the next appointment by itself — it only suggests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
hamed
2026-08-07 15:44:22 +03:30
co-authored by Claude Opus 5
parent d857242145
commit c7a3b88b32
9 changed files with 308 additions and 4 deletions
+35
View File
@@ -281,6 +281,41 @@ single-session again. Idempotent: deleting a service that has no protocol still
---
## GET `/api/v1/treatment-session/{uuid}`
یک جلسه به‌تنهایی، به‌علاوهٔ `case_uuid`، `service` و `patient` — برای فرمِ «ثبت نوبت این
جلسه». جلسهٔ محیط دیگر `404` می‌گیرد.
---
## بستنِ نوبت به یک جلسهٔ مشخص
یک بیمار می‌تواند چند دورهٔ باز داشته باشد. تا پیش از این، اتصال **ضمنی** بود: هنگام
قطعی‌شدن، از روی سرویسِ نوبت پروندهٔ باز پیدا می‌شد و نوبت به اولین جلسهٔ بدون‌نوبتِ آن
می‌چسبید. انتخابِ سرویسِ اشتباه بی‌صدا یک پروندهٔ موازی می‌ساخت.
`POST /api/v1/my/appointment` حالا `treatment_session_uuid` اختیاری می‌گیرد:
| کد | HTTP | شرط |
|---|---|---|
| ERR_NOT_FOUND_001 | 404 | جلسه یافت نشد یا مال محیط دیگری است |
| ERR_CONFLICT_001 | 409 | جلسه از قبل نوبت دارد |
| ERR_CONFLICT_001 | 409 | پروندهٔ جلسه باز نیست |
| ERR_CONFLICT_001 | 409 | جلسه مالِ بیمار دیگری است |
وقتی فرستاده شود، همان جلسه رزرو می‌شود و منطقِ «اولین جلسهٔ بدون نوبت» هنگام تأیید
کنار می‌رود. نبودنش یعنی همان رفتار قبلی.
### آزاد شدن جلسه
`cancelled_by_user` · `cancelled_by_doctor` · `no_show` جلسه را از نوبت جدا می‌کنند و به
`planned` برمی‌گردانند، پس دوباره در «جلسات بدون نوبت» دیده می‌شود. غیبت در
`CANCEL_STATUSES` نیست ولی برای دوره فرقی ندارد — سابقهٔ غیبت روی خودِ نوبت می‌ماند.
جلسهٔ `done` استثناست: سابقه است و آزاد نمی‌شود.
---
## GET `/api/v1/treatment-sessions/unbooked`
صفِ «جلسات بدون نوبت» — جلسه‌ای که سررسیدش رسیده و کسی رزروش نکرده.