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 {