Files
clinicpro/.claude/prompt/session-autofill-on-appointment-confirm.md
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

9.5 KiB

پرکردن خودکار پرونده (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 فقط می‌سازد و ذخیره می‌کند، بدون هیچ داده‌ای:

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() (چطور باید پر شود) — خطوط کلیدی:

// 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 پر کن:

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