From 69beb7b94460e8067c9617d282d8c94b3a2cf2b5 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Wed, 24 Jun 2026 06:09:05 +0330 Subject: [PATCH] feat(insurance): add insurance coverage flag to selected services and update coverage logic --- assets/admin/pages/NewSessionPage.tsx | 6 ++++-- docs/api/insurance.md | 2 +- src/Insurance/Service/TenantInsuranceService.php | 8 ++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/assets/admin/pages/NewSessionPage.tsx b/assets/admin/pages/NewSessionPage.tsx index 6077f12a..29bb3b8b 100644 --- a/assets/admin/pages/NewSessionPage.tsx +++ b/assets/admin/pages/NewSessionPage.tsx @@ -61,7 +61,7 @@ export default function NewSessionPage() { const nav = useNavigate(); const qc = useQueryClient(); - const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string; price: number; qty: number }[]>([]); + const [selectedServices, setSelectedServices] = useState<{ uuid: string; name: string; price: number; qty: number; insured: boolean }[]>([]); const [sectionUuid, setSectionUuid] = useState(''); const [itemUuid, setItemUuid] = useState(''); const [baseId, setBaseId] = useState(''); @@ -154,7 +154,7 @@ export default function NewSessionPage() { if (!itemUuid) return; const found = itemsData?.data?.find(i => i.uuid === itemUuid); if (!found || selectedServices.some(s => s.uuid === found.uuid)) return; - setSelectedServices(p => [...p, { uuid: found.uuid, name: found.name, price: found.price_rials, qty: 1 }]); + setSelectedServices(p => [...p, { uuid: found.uuid, name: found.name, price: found.price_rials, qty: 1, insured: !!found.insurance_covered }]); setItemUuid(''); }; @@ -168,6 +168,8 @@ export default function NewSessionPage() { const servicesPatient = useMemo( () => selectedServices.reduce((sum, x) => { const total = x.price * x.qty; + // پرچم سرویس gate نهایی: اگر «شامل بیمه» نباشد، کامل سهم بیمار است. + if (!x.insured) return sum + total; return sum + patientShareOf( total, ruleFor(baseContract, baseCoverage, x.uuid), diff --git a/docs/api/insurance.md b/docs/api/insurance.md index abffa158..c8ab9992 100644 --- a/docs/api/insurance.md +++ b/docs/api/insurance.md @@ -455,4 +455,4 @@ override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فی پاسخ `200`: `{ success, data: { message } }`. خطاها: `404 ERR_NOT_FOUND_001` قرارداد یافت نشد · `422 ERR_VALIDATION_001` سرویس یافت نشد · `403 ERR_FORBIDDEN_001` سرویس متعلق به شما نیست. -> منطق resolve: `TenantInsuranceService::coverageRuleForService()` ابتدا override خدمت را بررسی می‌کند؛ اگر `covered=false` → `CoverageRule::notCovered()`؛ در غیر این صورت فیلدهای null از قرارداد پر می‌شوند. این `CoverageRule` ورودی `BillingCalculator` است و سهم بیمه‌ی هر `InvoiceItem` را تعیین می‌کند؛ همان سهم‌ها در `ClaimService::createFromInvoice()` به `ClaimItem` (مطالبات بیمه) تبدیل می‌شوند. پنل ادمین این endpoint را از مودال «پوشش بیمه» در صفحه سرویس‌های کلینیک فراخوانی می‌کند. +> منطق resolve: `TenantInsuranceService::coverageRuleForService()` ابتدا **پرچم `ServiceItem.insurance_covered`** را چک می‌کند؛ اگر این خدمت «شامل بیمه» نباشد، بدون توجه به override یا قرارداد، `CoverageRule::notCovered()` برمی‌گردد (gate نهایی). سپس override خدمت بررسی می‌شود؛ اگر `covered=false` → `notCovered()`؛ در غیر این صورت فیلدهای null از قرارداد پر می‌شوند. این `CoverageRule` ورودی `BillingCalculator` است و سهم بیمه‌ی هر `InvoiceItem` را تعیین می‌کند؛ همان سهم‌ها در `ClaimService::createFromInvoice()` به `ClaimItem` (مطالبات بیمه) تبدیل می‌شوند. پنل ادمین این endpoint را از مودال «پوشش بیمه» در صفحه سرویس‌های کلینیک فراخوانی می‌کند. diff --git a/src/Insurance/Service/TenantInsuranceService.php b/src/Insurance/Service/TenantInsuranceService.php index 239182ee..a303cc77 100644 --- a/src/Insurance/Service/TenantInsuranceService.php +++ b/src/Insurance/Service/TenantInsuranceService.php @@ -2,6 +2,7 @@ namespace App\Insurance\Service; +use App\ClinicService\Repository\ServiceItemRepository; use App\Insurance\Entity\TenantInsurance; use App\Insurance\Repository\InsuranceRepository; use App\Insurance\Repository\TenantInsuranceRepository; @@ -16,6 +17,7 @@ class TenantInsuranceService private readonly TenantInsuranceRepository $repo, private readonly InsuranceRepository $insuranceRepo, private readonly TenantServiceCoverageRepository $coverageRepo, + private readonly ServiceItemRepository $serviceItemRepo, ) {} /** @@ -101,6 +103,12 @@ class TenantInsuranceService return CoverageRule::notCovered(); } + // پرچم سرویس gate نهایی است: اگر «این خدمت شامل بیمه می‌شود» غیرفعال باشد، هیچ پوششی محاسبه نمی‌شود. + $service = $this->serviceItemRepo->find($serviceItemId); + if ($service === null || !$service->isInsuranceCovered()) { + return CoverageRule::notCovered(); + } + $contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId); if ($contract === null) { return CoverageRule::notCovered();