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(