# جریان کاربری — تسک ۰۷ ## الف) مسیر موفق — بیمار از سایت عمومی ``` [تسک ۰۶] بیمار ساعت ۰۹:۰۰ را انتخاب می‌کند │ ▼ POST /api/v1/appointment-hold { "doctor_uuid":"…", "branch_uuid":"…", "service_item_uuid":"…", "option_uuids":["…"], "start": 1754…, "assignment": { "operator":"…", "room":"…", "device":"…" } } │ ├─ سرور: برنامه را دوباره می‌سازد (به assignment اعتماد نمی‌کند) ├─ نوبت pending با expires_at = now + 900 ├─ appointment_segments × ۵ └─ resource_occupancy × (بخش × منبع) — بخش انتظار فقط اتاق ▼ 201 { "hold_uuid":"…", "expires_at": 1754…, "total_price_rials": … } │ │ ⏱ تایمر ۱۵ دقیقه‌ای در UI: «۱۴:۵۹ برای تکمیل رزرو» ▼ پرداخت بیعانه (اگر deposit_required) → درگاه → بازگشت ▼ POST /api/v1/appointment-confirm { "hold_uuid":"…" } │ ├─ ۱ hold معتبر است؟ ├─ ۲ قوانین صلاحیت و فاصله (تسک ۰۹) ├─ ۳ وضعیت → confirmed ├─ ۴ اشغال‌ها hold → booked ├─ ۵ snapshot قیمت (تسک ۰۸) └─ ۶ رویداد AppointmentBooked (بعد از commit) ▼ 200 { "appointment_uuid":"…", "status":"confirmed" } ▼ پیامک تأییدیه (از راه رویداد، async) ``` --- ## ب) رقابت روی ساعت پرتقاضا ``` بیمار الف بیمار ب │ ۰۹:۰۰ را می‌بیند │ ۰۹:۰۰ را می‌بیند │ │ ▼ POST /appointment-hold ▼ POST /appointment-hold │ │ │ INSERT slot (res=7,b=…,u=0) ✓ │ INSERT slot (res=7,b=…,u=0) ✗ duplicate ▼ ▼ 201 hold_uuid 409 ERR_SLOT_TAKEN │ ▼ UI: «این ساعت همین لحظه رزرو شد.» + لیست به‌روزشدهٔ وقت‌های نزدیک (خودکار، بدون کلیک دوباره) ``` پیام «این ساعت همین لحظه رزرو شد» + پیشنهاد جایگزین، همان کاری است که مستند بند ۱۷ برای ریسک «رقابت روی ساعت‌های پرتقاضا» می‌خواهد. `409` خالی بدون جایگزین یعنی بیمار می‌رود. --- ## ج) hold منقضی می‌شود ``` hold ساخته شد ─── ۱۵ دقیقه ───▶ منقضی │ ┌─────────────────────────┴──────────────────────────┐ │ │ cron هر دقیقه یا: بیمار confirm می‌زند ExpireAppointmentsHandler │ │ ▼ ├─ status → expired 409 ERR_HOLD_EXPIRED ├─ occupancy → released │ └─ occupancy_slot → DELETE ▼ ▼ UI: «زمان رزرو شما به پایان رسید» زمان دوباره در جستجو ظاهر می‌شود + بازگشت به لیست وقت‌ها ``` نکته: حتی پیش از اجرای cron، `hasRoom` تسک ۰۶ شرط `expires_at > now` را دارد، پس hold مردهٔ چند ثانیه‌ای هم مانع کسی نمی‌شود. cron فقط تمیزکاری است. --- ## د) منشی نوبت را جابه‌جا می‌کند ``` پنل › نوبت‌ها › جزئیات نوبت › «جابه‌جایی» │ ▼ انتخاب تاریخ/ساعت جدید (همان UI تسک ۰۶، با forManagement=true) ▼ POST /api/v1/appointment/{uuid}/reschedule { "start": … , "assignment": {…} } │ ├─ ۱ hold جدید ساخته می‌شود ← اگر شکست: rollback کامل، نوبت قدیم سالم ├─ ۲ اشغال‌های قدیم released ├─ ۳ نوبت قدیم → rescheduled └─ ۴ لینک قدیم ↔ جدید در appointment_events ▼ 200 { "new_appointment_uuid": "…" } ▼ پیامک اطلاع‌رسانی جابه‌جایی ``` اگر hold جدید `409` بدهد: ``` 422 { "errors": [{ "code":"ERR_SLOT_TAKEN", "message":"زمان جدید در دسترس نیست. نوبت فعلی تغییری نکرد." }] } ``` جملهٔ دوم پیام اجباری است — منشی باید بداند وضعیت فعلی امن است و لازم نیست چیزی را درست کند. --- ## ه) لغو نوبت ``` لغو (بیمار یا پزشک یا منشی) │ ├─ status → cancelled_by_user / cancelled_by_doctor ├─ active_slot_key → NULL (مکانیزم موجود، دست‌نخورده) ├─ resource_occupancy → released └─ resource_occupancy_slot → DELETE ▼ ظرفیت فوری آزاد می‌شود ▼ [تسک ۱۳] لیست انتظار همان بازه اطلاع می‌گیرد ``` --- ## و) مسدودسازی دستی یک منبع حالت خاصی که `appointment_id = NULL` را توضیح می‌دهد: ``` پنل › منابع › دستگاه کندلا ۲ › «مسدودسازی بازه» تاریخ/ساعت + دلیل: «سرویس دوره‌ای» ▼ یک ردیف resource_occupancy با appointment_id=NULL و status='booked' (یا معادلاً یک resource_exception از تسک ۰۳ — هر دو کار می‌کنند) ``` **تصمیم:** مسدودسازی **بلندمدت و تکرارشونده** → `resource_exception` (تسک ۰۳). مسدودسازی **موردی و کوتاه** → `resource_occupancy` با `appointment_id=NULL`. دلیل: دومی در همان ایندکس داغ می‌نشیند و در `OccupancyIndex` بدون کد اضافه دیده می‌شود. این تفکیک را در `docs/api/appointment-booking.md` بنویس، وگرنه دو راه انجام یک کار گیج‌کننده می‌شود.