# پرکردن خودکار پرونده (Session) هنگام قطعی شدن نوبت: تاریخ/ساعت مراجعه + آیتم‌های هزینه + جمع کل

## پروژه

`clinicpro` (backend Symfony + پنل ادمین React). تک-ریپو.

> تست: پنل ادمین با `09390039833 / 09390039833`. اجرا داخل ddev.

## زمینه

وقتی نوبت به وضعیت **قطعی/تأیید (`confirmed`)** تغییر می‌کند، `AppointmentController` تابع `PatientService::autoCreateOnAppointmentConfirm()` را صدا می‌زند تا برای بیمار یک پرونده (`PatientSession`) بسازد. اما مسیر auto-create فقط `new PatientSession($record, $appointment)` می‌سازد و ذخیره می‌کند — **هیچ داده‌ی هزینه یا زمان مراجعه‌ای ثبت نمی‌شود**. در نتیجه پرونده‌ی ساخته‌شده از نوبت: `session_at = null` (به created_at برمی‌گردد)، `visit_price_rials = 0`، `services_total_rials = 0`، `final_price_rials = 0` و لیست سرویس‌ها خالی است — درحالی‌که خودِ `Appointment` هم `visit_price_rials` و هم `serviceItems` را دارد. این یک باگ است.

مسیر دیگر (ویزارد دستیِ ثبت مراجعه `createSession()`) همه‌ی این‌ها را درست پر می‌کند و **الگوی مرجع** است.

## هدف

مسیر auto-create پرونده از نوبت باید مثل ویزارد، این‌ها را خودکار و **بدون ورود دستی قیمت** پر کند:
1. **تاریخ/ساعت مراجعه** (`session_at`) از زمان واقعی نوبت (`Appointment::getSlotStart()`).
2. **آیتم‌های هزینه‌ی سرویس** به‌صورت تفکیک‌شده: برای هر سرویسِ نوبت یک `SessionService` با قیمت snapshot از خود سرویس (`ServiceItem::getPriceRials()`).
3. **هزینه ویزیت** (`visit_price_rials`) از نوبت (و اگر نوبت مقدار نداشت، از تنظیم «قیمت ویزیت آزاد»).
4. **جمع کل**: `services_total_rials` = مجموع خطوط سرویس؛ `final_price_rials` = `services_total_rials + visit_price_rials`.
5. نمایش در UI پرونده: تاریخ/ساعت مراجعه به‌عنوان اولین اطلاعات + جدول تفکیک‌شده‌ی هزینه‌ها (ویزیت + هر سرویس) + جمع کل.

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

| فایل | نقش |
|------|-----|
| `src/Patient/Service/PatientService.php` | `autoCreateOnAppointmentConfirm()` (L94-135) و `autoCreateForEntity()` (L119-135) — **محل باگ**؛ `createSession()` (L137-237) الگوی مرجع |
| `src/Patient/Entity/PatientSession.php` | Setterها: `setSessionAt()` (L204)، `setVisitPriceRials()` (L190)، `setServicesTotalRials()` (L193)، `setFinalPriceRials()` (L194)، `addService()` (L121) |
| `src/Patient/Entity/SessionService.php` | خط هزینه؛ constructor قیمت را از `ServiceItem::getPriceRials()` snapshot می‌کند (L44-53)؛ `getLineTotalRials()` (L62) |
| `src/ClinicService/Entity/ServiceItem.php` | `getPriceRials()` (L87) — منبع قیمت سرویس |
| `src/Appointment/Entity/Appointment.php` | `getSlotStart()`، `getVisitPriceRials()` (L251)، `getServiceItems()` (L235، ManyToMany `appointment_service_items`) |
| `src/Insurance/Service/VisitPriceRequirementResolver.php` | resolve تنظیم ویزیت (L22-37) — منبع fallback قیمت ویزیت آزاد |
| `src/Insurance/Entity/EntityInsurancePricing.php` | ردیف free-visit (`insurance_id = null`): `getPatientShareRials()` (L54) = قیمت ویزیت آزاد پیش‌فرض |
| `assets/admin/components/session/DetailsStep.tsx` | نمایش خلاصه پرونده — الان `session_at` و جدول تفکیکی ندارد |
| `docs/api/patient.md`، `docs/api/appointment.md` | به‌روزرسانی مستندات (Standing Rule) |

## وضعیت فعلی (باگ)

`src/Patient/Service/PatientService.php` — auto-create فقط می‌سازد و ذخیره می‌کند، بدون هیچ داده‌ای:

```php
private function autoCreateForEntity(string $entityType, int $entityId, Appointment $appointment, int $createdById): void
{
    if (!$this->subscriptionService->hasFeature($entityType, $entityId, 'patient_records')) {
        return;
    }
    $patient = $appointment->getUser();
    $record = $this->recordRepo->findByEntityAndUser($entityType, $entityId, $patient);
    if ($record === null) {
        $record = new PatientRecord($entityType, $entityId, $patient, 'system', $createdById);
        $this->recordRepo->save($record);
    }
    $session = new PatientSession($record, $appointment);   // ← هیچ‌چیز دیگر ست نمی‌شود
    $this->sessionRepo->save($session);
}
```

الگوی مرجع در `createSession()` (چطور باید پر شود) — خطوط کلیدی:

```php
// visit price
$session->setVisitPriceRials((int) ($data['visit_price_rials'] ?? 0));
// session_at (زمان مراجعه)
if (!empty($data['session_at'])) { $session->setSessionAt((int) $data['session_at']); }
// خطوط سرویس + مجموع
foreach ($data['services'] as $s) {
    $item = $this->serviceItemRepo->findByUuid($s['uuid']);
    $session->addService(new SessionService($session, $item, $staff, (int)($s['qty'] ?? 1)));
}
$session->setServicesTotalRials($servicesTotal);
$session->setFinalPriceRials($servicesTotal + $visitPrice + ...);
```

## وظایف

### ۱. پرکردن پرونده‌ی auto-create از روی نوبت (backend)

در `autoCreateForEntity()` بعد از ساخت `$session` و **قبل از** `save()`، از `$appointment` پر کن:

```php
$session = new PatientSession($record, $appointment);

// ۱) زمان مراجعه = زمان واقعی نوبت
$session->setSessionAt($appointment->getSlotStart());

// ۲) هزینه ویزیت: از نوبت، fallback به «قیمت ویزیت آزاد» تنظیمات
$visitPrice = $appointment->getVisitPriceRials()
    ?? $this->resolveFreeVisitPrice($entityType, $entityId);
$session->setVisitPriceRials($visitPrice ?? 0);

// ۳) خطوط سرویس تفکیک‌شده (قیمت snapshot از خود سرویس)
$servicesTotal = 0;
foreach ($appointment->getServiceItems() as $item) {
    $line = new SessionService($session, $item, null, 1); // قیمت از ServiceItem::getPriceRials()
    $session->addService($line);
    $servicesTotal += $line->getLineTotalRials();
}

// ۴) جمع کل
$session->setServicesTotalRials($servicesTotal);
$session->setFinalPriceRials($servicesTotal + ($visitPrice ?? 0));

$this->sessionRepo->save($session);
```

- `resolveFreeVisitPrice($entityType, $entityId)`: یک helper که ردیف free-visit (`insurance_id = null`) را برای این doctor/clinic از `pricingRepo->findOneForInsurance(TYPE_DOCTOR|TYPE_CLINIC, $entityId, null)` می‌خواند و `getPatientShareRials()` را برمی‌گرداند (یا null). از منطق موجود `VisitPriceRequirementResolver` الگو بگیر.
- **توجه به مسیر دوگانه**: `autoCreateOnAppointmentConfirm` این متد را هم برای `doctor` و هم (در صورت وجود) `clinic` صدا می‌زند، پس ممکن است **دو پرونده** ساخته شود (یکی برای پزشک، یکی برای کلینیک). این رفتار فعلی است؛ آن را تغییر نده، فقط هر دو را درست پر کن.
- **قیمت دستی وارد نشود** — همیشه از `ServiceItem::getPriceRials()` و تنظیم ویزیت خوانده شود.

### ۲. نمایش تاریخ/ساعت مراجعه + جدول هزینه تفکیکی در UI پرونده (frontend)

در `assets/admin/components/session/DetailsStep.tsx`:
- **اولین اطلاعات**: «تاریخ و ساعت مراجعه» از `session_at` (شمسی با `formatDateTime`/`formatDate` — Unix timestamp صحیح). اگر `session_at` خالی بود، از `created_at`.
- **جدول هزینه‌ی تفکیک‌شده**: یک ردیف «ویزیت: {formatRial(visit_price_rials)}» وقتی `visit_price_rials > 0`، سپس هر خط سرویس از آرایه‌ی `services` (`service_name` + `line_total_rials`)، و در انتها «جمع کل: {final_price_rials}». از `toArray()` پرونده که `services`، `visit_price_rials`، `services_total_rials`، `final_price_rials`، `session_at` را می‌دهد استفاده کن.

## نکات مهم

- تاریخ‌ها Unix timestamp صحیح (`setSessionAt(int)`), نمایش شمسی با `formatDate`/`formatDateTime`.
- قیمت‌ها ریالی ذخیره؛ نمایش با `formatRial`. تبدیل تومان↔ریال با `tomanToRial`/`rialToToman`.
- تغییری در `Appointment` یا schema لازم نیست (فقط خواندن)؛ **بدون migration** مگر بخواهی ستون audit اضافه کنی (لازم نیست).
- تست: یک نوبت را از `pending` به `confirmed` ببر (`PATCH /api/v1/appointment/{uuid}/status`) و بررسی کن پرونده‌ی ساخته‌شده `session_at`، `visit_price_rials`، خطوط سرویس و `final_price_rials` درست دارد (endpoint `GET /api/v1/patient/{recordUuid}/sessions`).
- بعد از تغییر، `docs/api/patient.md` را اگر خروجی session تغییر معنایی کرد به‌روز کن.
- این پرامپت **پیش‌نیاز منطقی** پرامپت `discount-rules-engine.md` است (تخفیف روی `final_price_rials` اعمال می‌شود که اینجا درست می‌شود).
