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>
125 lines
9.5 KiB
Markdown
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` اعمال میشود که اینجا درست میشود).
|