# فرآیند ثبت و قطعی کردن نوبت (مودال پرداخت + پرونده)

## پروژه

`clinicpro` (backend + پنل ادمین) — **پیش‌نیاز:** `clinic-appointment-operations-fix.md` اجرا شده باشد.

## زمینه

وضعیت‌ها همین حالا وجود دارند: `pending` = «ثبت شده»، `confirmed` = «قطعی شده» (`turnStatus.ts`). زیرساخت پرونده هم هست: با confirm شدن نوبت، `AppointmentConfirmationService::onConfirmed` → `PatientService::autoCreateOnAppointmentConfirm` پرونده را بر اساس محیط (`clinic` اگر `appointment.getClinic()!==null` وگرنه `doctor`) **پیدا یا ایجاد** می‌کند و session با قیمت ویزیت + سرویس‌ها می‌سازد — یعنی الزام «پرونده موجود استفاده شود / نبود ساخته شود» از قبل پیاده است. پرداخت چندبخشی هم روی session موجود است (`SessionPayment`، متدهای `wallet/pos/cash/card`).

آنچه کم است: (۱) نوبت پنلی الان مستقیم `confirmed` ساخته می‌شود؛ (۲) دکمه/مودال «قطعی کردن نوبت» با نمایش هزینه‌ها و پرداخت کامل/جزئی وجود ندارد؛ (۳) ثبت پرداخت‌ها هنگام قطعی شدن در پرونده انجام نمی‌شود.

## مشکل / هدف

1. هر نوبت (آنلاین، سریع، عادی) با وضعیت اولیه «ثبت‌شده» (`pending`) ایجاد شود.
2. روی کارت نوبت‌های `pending` در Timeline دکمه «قطعی کردن نوبت» باشد.
3. کلیک → مودال: مبلغ ویزیت + هزینه سرویس‌های انتخاب‌شده، پرداخت کامل یا جزئی، نمایش شفاف پرداخت‌شده/باقی‌مانده/وضعیت پرداخت.
4. تأیید مودال → وضعیت `confirmed` + ثبت سرویس‌ها و پرداخت‌ها در پرونده (موجود یا جدید) نزد همان محیط.

## فایل‌های مرتبط

| فایل | نقش |
|------|-----|
| `src/Appointment/Entity/Appointment.php` | وضعیت‌ها (~25-33)، `ALLOWED_TRANSITIONS` (~37-42)، `visitPriceRials`، `serviceItems` |
| `src/Appointment/Controller/MyAppointmentsController.php` | ساخت پنلی — الان `confirmed` می‌گذارد (~192) |
| `src/Appointment/Controller/AppointmentController.php` | `PATCH .../status` (~850)؛ endpoint جدید confirm اینجا یا کنارش |
| `src/Appointment/Repository/AppointmentRepository.php` | `expireLapsedPending` (~154) — TTL پانزده‌دقیقه‌ای pending |
| `src/Appointment/Service/AppointmentConfirmationService.php` | `onConfirmed` (~30) — نقطه واحد confirm |
| `src/Patient/Service/PatientService.php` | `autoCreateOnAppointmentConfirm` (~133)، `addSessionPayment` (~552) |
| `src/Payment/Service/PaymentManager.php` | مسیر آنلاین: بعد از پرداخت درگاه → `confirmed` (~306-315) — دست نزن |
| `assets/admin/components/appointments/TurnsTimeline.tsx` | کارت‌ها (`OccupiedCard` ~100) |
| `assets/admin/components/appointments/turnStatus.ts` | لیبل‌ها (pending=«ثبت شده») |
| `assets/admin/components/ui/AppointmentStatusDropdown.tsx` | `TRANSITIONS` + `PATCH status {status, version}` |
| `assets/admin/components/session/PaymentStep.tsx` | الگوی پرداخت جزئی (`METHODS`, `METHOD_LABELS`, `PriceInput`, toman→rial) |
| `assets/admin/components/ui/Modal.tsx`, `ConfirmDialog.tsx` | پایه مودال |
| `assets/admin/pages/AppointmentCreatePage.tsx` | گزینه‌های status هنگام ساخت (~496) |

## وضعیت فعلی

```php
// MyAppointmentsController (~192): نوبت پنلی بلافاصله confirmed
$appointment->setStatus(Appointment::STATUS_CONFIRMED);

// PaymentManager (~313): نوبت سایت بعد از پرداخت درگاه confirmed می‌شود (درست است، حفظ شود)
// AppointmentRepository::expireLapsedPending: pending های کهنه را expire می‌کند (TTL رزرو آنلاین ۱۵ دقیقه)
```

```tsx
// AppointmentStatusDropdown (~74): تنها مسیر فعلی قطعی‌کردن — بدون پرداخت/پرونده
api.patch(`/api/v1/appointment/${uuid}/status`, { status: newStatus, version })
```

## وظایف

### ۱. Backend — ساخت پنلی با وضعیت `pending` بدون انقضا

- در `MyAppointmentsController::create` وضعیت اولیه را `STATUS_PENDING` کن (نوبت سریع و عادی).
- **حیاتی:** `expireLapsedPending` نباید نوبت‌های پنلی را بعد از ۱۵ دقیقه منقضی کند. مکانیزم تفکیک اضافه کن — مثلاً فیلد/فلگ `source`/`hold_expires_at` روی Appointment (migration) یا شرط «pending فقط وقتی expire شود که از مسیر رزرو آنلاین با TTL ساخته شده». مسیر آنلاین (POST `/api/v1/appointment` عمومی) رفتار فعلی‌اش (pending با TTL تا پرداخت درگاه) را حفظ کند.
- گذار `pending → confirmed` از قبل در `ALLOWED_TRANSITIONS` مجاز است — دست نزن.

### ۲. Backend — endpoint قطعی‌کردن اتمیک

`POST /api/v1/appointment/{uuid}/confirm` بساز (در `AppointmentController`، با `canManage` از checker پرامپت قبلی):

```php
// Request:
// { "version": 3, "payments": [ { "method": "cash|pos|card|wallet", "amount_rials": 500000 } ], "discount"?: ... }
// در یک تراکنش:
// 1) transitionTo(STATUS_CONFIRMED)  → از canTransitionTo عبور کند
// 2) AppointmentConfirmationService::onConfirmed($appointment) → record/session (منطق موجود reuse/create)
// 3) session ساخته/یافته‌شده را بگیر و هر payment را با PatientService::addSessionPayment ثبت کن
// Response: success + { appointment: {...}, session: { uuid, final_price_rials, paid_total_rials, remaining_rials, is_paid } }
```

- `payments` می‌تواند خالی باشد (قطعی بدون پرداخت) یا جزئی — جمع نباید از مبلغ قابل‌پرداخت بیشتر شود (خطای موجود `ERR_SESSION_PAYMENT_EXCEEDS` reuse شود).
- `autoCreateOnAppointmentConfirm` الان خطا را قورت می‌دهد (log-only). برای این endpoint نباید silent باشد: اگر پرونده/سرویس‌ها ساخته نشد (مثلاً feature اشتراک `patient_records` فعال نیست)، پاسخ باید صریح بگوید (confirm موفق ولی `session: null` + پیام، یا خطای کامل — تصمیم را مستند کن).
- endpoint یک GET پیش‌نمایش هم لازم دارد یا همان detail کافی است: مودال باید مبلغ ویزیت (`visit_price_rials`) + سرویس‌های نوبت (`serviceItems` با قیمت) را قبل از تأیید نشان دهد — اگر detail فعلی قیمت آیتم‌ها را نمی‌دهد، به پاسخ detail اضافه کن.

### ۳. Frontend — دکمه و مودال «قطعی کردن نوبت»

- در `TurnsTimeline.tsx` روی `OccupiedCard` وقتی `a.status === 'pending'` دکمه «قطعی کردن نوبت» اضافه کن (کنار کلاستر dropdown/menu، با `stopPropagation`).
- مودال جدید `components/appointments/ConfirmAppointmentModal.tsx` بر پایه `Modal` (نه ConfirmDialog — فرم دارد):
  - بخش هزینه‌ها: ردیف «ویزیت» + ردیف هر سرویس انتخاب‌شده + جمع کل (`formatRial`، نمایش تومان مثل `PaymentStep`).
  - بخش پرداخت: همان الگوی `PaymentStep` — روش‌ها (`METHODS`/`METHOD_LABELS`)، `PriceInput` تومان، امکان چند ردیف پرداخت یا یک ردیف با مبلغ دلخواه؛ دکمه میان‌بر «پرداخت کامل».
  - خلاصه شفاف: پرداخت‌شده / باقی‌مانده / وضعیت (تسویه کامل، پرداخت جزئی، بدون پرداخت).
  - تأیید → `POST /api/v1/appointment/${uuid}/confirm` با `version`؛ بعد `invalidateQueries({ queryKey })`؛ toast موفقیت با sonner؛ خطای 409 نسخه با پیام فارسی.
- همین دکمه/مودال را در `AppointmentDetailPage`، `ReserveAppointmentsPage` (ردیف‌های pending) و `AppointmentInfoModal` هم در دسترس بگذار.
- در `AppointmentStatusDropdown`، انتخاب مستقیم `confirmed` از dropdown باید همین مودال را باز کند (نه PATCH خام) تا مسیر دورزدن پرداخت/پرونده نماند — یا حداقل بعد از PATCH خام هم `onConfirmed` سمت سرور اجرا می‌شود (الان می‌شود؛ ولی بدون پرداخت). تصمیم UX: dropdown → مودال. مستند کن.
- `AppointmentCreatePage` (~496): پیش‌فرض ساخت را «ثبت شده» بگذار؛ گزینه ساخت مستقیم confirmed را بردار یا به مودال وصل کن.

### ۴. تست و مستندات

- سناریوها: قطعی با پرداخت کامل / جزئی / بدون پرداخت؛ بیمار با پرونده قبلی نزد همان پزشک (reuse — session جدید در همان پرونده) و بیمار بدون پرونده (create)؛ همین دو حالت در محیط کلینیک (`entityType=clinic`) با کاربر `09024206041` و در مطب شخصی با کاربر پزشک از `TEST_USERS.md`.
- رزرو آنلاین سایت: بدون رگرسیون — pending تا پرداخت درگاه، بعد confirmed + پرونده (مسیر `PaymentManager` دست‌نخورده).
- نوبت‌های `is_reserve` مثل قبل از `onConfirmed` رد می‌شوند (خط ~33) — دکمه قطعی‌کردن برای ردیف رزرو روزانه بعد از انتقال به slot معنا پیدا می‌کند.
- `docs/api/*`: endpoint جدید confirm + تغییر رفتار create مستند شود.

## نکات مهم

- تاریخ‌ها Unix timestamp؛ نمایش شمسی با `formatDate()`. مبالغ backend ریال، ورودی UI تومان (`tomanToRial`).
- Optimistic lock: هر mutation نوبت `version` می‌خواهد؛ فراموشش نکن (AppointmentDetailPage الان status را بدون version می‌فرستد — همان‌جا هم اصلاح کن).
- envelope پاسخ: single ممکن است double-nested باشد (`data?.data?.data`) — الگوی صفحات موجود را نگاه کن.
- لیبل‌های فارسی موجود را تغییر نده: `pending`=«ثبت شده»، `confirmed`=«قطعی شده». دو map وضعیت موازی هست (`turnStatus.ts` و `AppointmentStatusDropdown.STATUS_META`) — اگر دست زدی هر دو را همگام نگه دار.
- کامپوننت انتخاب‌ها فقط `SearchableSelect`؛ طراحی مودال با تم/کلاس‌های موجود پنل، بدون طراحی جدید.
