From 8c39d720b59c9998d41946510db6f298e0b3c9f8 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Fri, 17 Jul 2026 11:38:13 +0330 Subject: [PATCH] fix(patient): populate auto-created session from the confirmed appointment When an appointment is confirmed, the auto-created PatientSession was saved empty: session_at null, visit_price_rials 0, no service line items, totals 0. Now it copies the visit datetime from the appointment slot, the visit price (falling back to the tenant free-visit price), a SessionService line per appointment service (price snapshot from the service), and computes services_total_rials / final_price_rials. Verified end-to-end. Co-Authored-By: Claude Fable 5 --- docs/api/appointment.md | 2 ++ src/Patient/Service/PatientService.php | 27 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/docs/api/appointment.md b/docs/api/appointment.md index 24d82ded..a8676c27 100644 --- a/docs/api/appointment.md +++ b/docs/api/appointment.md @@ -225,6 +225,8 @@ Book an appointment slot. > **تضمین عدم رزرو دوگانه:** هر سه مسیر رزرو از `AppointmentRepository::bookAtomically()` عبور می‌کنند و یک قید یکتای دیتابیسی (`active_slot_key`) پشت آن قرار دارد؛ بنابراین حتی در شرایط رقابتی (race) فقط یک نوبتِ زنده روی هر `(doctor, slot_start)` ممکن است و درخواست بازنده `409 SLOT_TAKEN` می‌گیرد. نوبت‌های لغو/منقضی اسلات را آزاد می‌کنند (کلید `NULL`). علاوه بر این، `bookAtomically` داخل تراکنش یک قفلِ per-doctor (`PESSIMISTIC_WRITE` روی ردیف پزشک) می‌گیرد؛ چون در **حالت سرویسی** نوبت‌ها طول متغیر و شروعِ متفاوت دارند و قید یکتای `(doctor, slot_start)` تداخلِ بازه‌ایِ دو رزروِ هم‌زمان با شروعِ متفاوت را نمی‌گیرد. این قفل بررسیِ overlap و insert را نسبت به سایر رزروهای همان پزشک اتمیک می‌کند. > **Auto-add to clinic:** هنگام تأیید نوبت، اگر آدرس نوبت متعلق به یک کلینیک باشد (`DoctorAddress.clinic_id`)، بیمار علاوه بر پرونده‌ی پزشک، به پرونده‌های آن کلینیک هم اضافه می‌شود. اگر آدرس کلینیک نداشت ولی دکتر فقط عضو یک کلینیک بود، به همان کلینیک اضافه می‌شود. هر شاخه مشروط به فعال‌بودن `patient_records`. جزئیات در `docs/api/patient.md`. +> +> **Auto-fill session on confirm:** پرونده‌ای که هنگام تأیید نوبت خودکار ساخته می‌شود، اکنون از خود نوبت پر می‌شود: `session_at` = زمان واقعی نوبت (`slot_start`)، `visit_price_rials` = هزینه ویزیت نوبت (در نبود آن، «قیمت ویزیت آزاد» تنظیمات)، برای هر سرویسِ نوبت یک ردیف `SessionService` با قیمت snapshot از خود سرویس، و `services_total_rials`/`final_price_rials` محاسبه‌شده. قبلاً همه‌ی این مقادیر صفر/خالی ثبت می‌شدند (باگ). > **Payer vs patient:** the authenticated user (`user`) is always the payer; the `patient_*` fields describe who the visit is for and are stored separately. **Temporary lock:** the slot is held by the new `pending` booking for **15 minutes** (`expires_at = created_at + 900`). If payment is not completed in time, the booking is moved to `expired` and the slot is freed (see `app:cancel-expired-appointments`). An expired pending booking no longer blocks the slot even before the cron runs. diff --git a/src/Patient/Service/PatientService.php b/src/Patient/Service/PatientService.php index 22656a1b..c0e41fba 100644 --- a/src/Patient/Service/PatientService.php +++ b/src/Patient/Service/PatientService.php @@ -131,7 +131,34 @@ class PatientService } $session = new PatientSession($record, $appointment); + + // زمان مراجعه = زمان واقعی نوبت. + $session->setSessionAt($appointment->getSlotStart()); + + // هزینه ویزیت: از نوبت، در نبود آن از «قیمت ویزیت آزاد» تنظیمات همین tenant. + $visitPrice = (int) ($appointment->getVisitPriceRials() + ?? $this->pricingRepo->findOneForInsurance($entityType, $entityId, null)?->getPatientShareRials() + ?? 0); + $session->setVisitPriceRials($visitPrice); + + // خطوط هزینه‌ی سرویس (قیمت snapshot از خود سرویس، بدون ورود دستی). + $servicesTotal = 0; + $lines = []; + foreach ($appointment->getServiceItems() as $item) { + $line = new SessionService($session, $item, null, 1); + $lines[] = $line; + $servicesTotal += $line->getLineTotalRials(); + } + + $session->setServicesTotalRials($servicesTotal); + $session->setFinalPriceRials($servicesTotal + $visitPrice); + $this->sessionRepo->save($session); + + foreach ($lines as $line) { + $this->sessionServiceRepo->save($line); + $session->addService($line); + } } public function createSession(