Files
clinicpro/.claude/prompt/session-autofill-on-appointment-confirm.md
T
hamedandClaude Fable 5 476e219165 fix(admin): convert toman to rial when recording session payment/discount
PaymentStep sent the toman amount straight through as amount_rials (and the
fixed discount value as discount_value), so a 500,000 toman payment was stored
as 5,000,000... no — as 500,000 rial (10x too small). Apply tomanToRial before
sending the payment amount and the fixed-discount value; percent discount and
rule-based discount are unaffected. Verified stored value is now correct rial.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:38:10 +03:30

125 lines
9.5 KiB
Markdown

# پرکردن خودکار پرونده (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` اعمال می‌شود که اینجا درست می‌شود).