From 6e3d574e3874ad70ee96052c8718902edcc99c52 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Tue, 18 Aug 2026 17:56:57 +0330 Subject: [PATCH] fix(session): show and keep the doctor's insurance when editing an encounter The encounter edit form asked for contracts in the panel's own environment, so inside a clinic it got an empty list: no insurance to choose, and the one already recorded on the encounter had no matching option to display. It now scopes the request by the encounter's doctor, the same way the confirm modal does, which is why the session payload's existing doctor_uuid is threaded through. Saving the form had the matching defect on the server. Recalculating the totals looked up the coverage percentage against the record's tenant rather than the contract's, so a single save on a correctly-billed encounter zeroed the insurance share and moved the whole amount back onto the patient. Co-Authored-By: Claude Opus 5 (1M context) --- .../admin/components/SessionServiceCard.tsx | 2 ++ .../admin/components/session/CreateStep.tsx | 13 ++++++-- src/Patient/Service/PatientService.php | 32 ++++++++++++++++--- ...linicAppointmentUsesDoctorContractTest.php | 28 ++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/assets/admin/components/SessionServiceCard.tsx b/assets/admin/components/SessionServiceCard.tsx index a2cf5515..ad2368cd 100644 --- a/assets/admin/components/SessionServiceCard.tsx +++ b/assets/admin/components/SessionServiceCard.tsx @@ -28,6 +28,8 @@ export interface SessionCardData { base_insurance_discount_percent?: number; supplementary_discount_percent?: number; doctor_name?: string | null; + /** پزشکِ نوبتِ این مراجعه — محدودهٔ قرارداد بیمه از روی او حل می‌شود. */ + doctor_uuid?: string | null; final_price_rials?: number; // تفکیک بیمه — سرور محاسبه می‌کند (PatientSession::applyShares) gross_total_rials?: number; diff --git a/assets/admin/components/session/CreateStep.tsx b/assets/admin/components/session/CreateStep.tsx index f2190713..2c8d2c97 100644 --- a/assets/admin/components/session/CreateStep.tsx +++ b/assets/admin/components/session/CreateStep.tsx @@ -105,11 +105,20 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e const { data: packagesData } = useQuery>({ queryKey: ['inventory-packages'], queryFn: () => api.get('/api/v1/inventory-packages'), }); + // مراجعهٔ برخاسته از نوبت، قراردادِ پزشکِ همان نوبت را دارد نه قراردادِ محیط پنل: + // در کلینیک، قرارداد روی خودِ پزشک ثبت می‌شود و بدون این محدوده، فهرست بیمه‌ها + // خالی می‌آمد و بیمهٔ ثبت‌شدهٔ همان مراجعه هم در فرم پیدا نمی‌شد. + const insuranceScope = editSession?.doctor_uuid + ? `?doctor_uuid=${encodeURIComponent(editSession.doctor_uuid)}&inherit=1` + : ''; + const { data: contractsData } = useQuery<{ data: { data: Contract[] } }>({ - queryKey: ['tenant-insurances'], queryFn: () => api.get('/api/v1/billing/tenant-insurances'), + queryKey: ['tenant-insurances', editSession?.doctor_uuid ?? null], + queryFn: () => api.get(`/api/v1/billing/tenant-insurances${insuranceScope}`), }); const { data: pricingData } = useQuery<{ data: { free_visit_price_rials: number; require_visit_price: boolean } }>({ - queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'), + queryKey: ['insurance-pricing', editSession?.doctor_uuid ?? null], + queryFn: () => api.get(`/api/v1/insurance-pricing${insuranceScope}`), }); const freeVisit = (pricingData as any)?.data?.free_visit_price_rials ?? 0; const requireVisit = (pricingData as any)?.data?.require_visit_price ?? false; diff --git a/src/Patient/Service/PatientService.php b/src/Patient/Service/PatientService.php index 6e6f41bc..b01bdab6 100644 --- a/src/Patient/Service/PatientService.php +++ b/src/Patient/Service/PatientService.php @@ -165,6 +165,27 @@ class PatientService ->coveragePercent; } + /** + * محیطی که قرارداد بیمهٔ این مراجعه در آن ثبت شده — اول پزشکِ نوبت، بعد کلینیکش. + * + * مراجعهٔ دستی نوبتی ندارد و پزشکی برای پرسیدن نیست، پس همان محیط پرونده می‌ماند. + * {@see \App\Insurance\Service\InsuranceScopeResolver} + * + * @return array{0: string, 1: int} + */ + private function coverageScopeOf(PatientSession $session, string $entityType, int $entityId): array + { + $appointment = $session->getAppointment(); + if ($appointment === null) { + return [$entityType, $entityId]; + } + + return $this->insuranceScope->forContracts( + (int) $appointment->getDoctor()->getId(), + $appointment->getClinic()?->getId() !== null ? (int) $appointment->getClinic()->getId() : null, + ); + } + /** * سهم‌های مراجعهٔ یک نوبت را دوباره حساب می‌کند — برای وقتی بیمهٔ نوبت *بعد از* * ساخته‌شدنِ مراجعه عوض می‌شود. @@ -543,14 +564,17 @@ class PatientService $session->getServices()->toArray(), ); $visitCategory = $session->getInsuranceServiceCategory(); - $session->setBaseInsuranceDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceBaseId(), $visitCategory)); - $session->setSupplementaryDiscountPercent($this->contractPercent($entityType, $entityId, $session->getInsuranceSupplementaryId(), $visitCategory)); + // درصد پوشش از محیطِ قرارداد می‌آید، نه محیطِ پرونده — همان قاعدهٔ ساخت خودکار. + [$coverageType, $coverageId] = $this->coverageScopeOf($session, $entityType, $entityId); + + $session->setBaseInsuranceDiscountPercent($this->contractPercent($coverageType, $coverageId, $session->getInsuranceBaseId(), $visitCategory)); + $session->setSupplementaryDiscountPercent($this->contractPercent($coverageType, $coverageId, $session->getInsuranceSupplementaryId(), $visitCategory)); $priceCalc = $this->calculateFinalPrice( $session->getVisitPriceRials(), $serviceItemsData, - $entityType, - $entityId, + $coverageType, + $coverageId, $session->getInsuranceBaseId(), $session->getInsuranceSupplementaryId(), $visitCategory, diff --git a/tests/Billing/ClinicAppointmentUsesDoctorContractTest.php b/tests/Billing/ClinicAppointmentUsesDoctorContractTest.php index 1a3a8b78..e8c927f3 100644 --- a/tests/Billing/ClinicAppointmentUsesDoctorContractTest.php +++ b/tests/Billing/ClinicAppointmentUsesDoctorContractTest.php @@ -111,6 +111,34 @@ class ClinicAppointmentUsesDoctorContractTest extends ApiTestCase self::assertSame(990_000, $claims[0]->getTotalClaimedRials()); } + /** + * ویرایش دستیِ همان مراجعه هم نباید سهم بیمه را دور بیندازد: فرم ویرایش سرویس‌ها + * کل مبلغ را دوباره حساب می‌کند و اگر درصد را از محیط اشتباه بگیرد، مراجعه‌ای که + * درست ساخته شده بود با یک ذخیرهٔ ساده خراب می‌شود. + */ + public function testEditingTheSessionKeepsTheInsuranceShare(): void + { + [$owner, $doctor, $clinic, $insurance] = $this->scenario(30); + $appointment = $this->appointment($doctor, $clinic, $insurance); + + $session = static::getContainer()->get(PatientService::class) + ->autoCreateOnAppointmentConfirm($appointment); + + $this->authJson('PATCH', '/api/v1/session/' . $session->getUuid(), $owner, [ + 'visit_price_rials' => self::VISIT_PRICE, + ]); + self::assertSame(200, $this->responseCode()); + + // درخواستِ کرنل روی EntityManager دیگری نوشت، پس ردیف دوباره خوانده می‌شود. + $row = $this->em->getConnection()->fetchAssociative( + 'SELECT base_insurance_rials, patient_share_rials FROM patient_sessions WHERE uuid = ?', + [$session->getUuid()], + ); + + self::assertSame(990_000, (int) $row['base_insurance_rials']); + self::assertSame(2_310_000, (int) $row['patient_share_rials']); + } + /** نوبتِ بدون قرارداد، مثل قبل کاملاً سهم بیمار می‌ماند. */ public function testAnAppointmentWithoutAnyContractStillBillsThePatientInFull(): void {