feat: integrate insurance coverage management for clinic services

- Updated NewSessionPage to calculate patient share based on insurance coverage rules.
- Refactored billing calculations to utilize new patientShareOf function for service items.
- Enhanced API documentation to reflect changes in service coverage structure.
- Implemented ServiceInsuranceModal for managing insurance coverage per service.
- Added UI components for displaying and editing insurance coverage details.
- Removed obsolete toggle switch styles and adjusted CSS for new components.
- Ensured backend endpoints support both service_item_id and service_item_uuid for flexibility.
This commit is contained in:
hamed
2026-06-24 04:23:50 +03:30
parent 253414ef3e
commit 27840da78d
11 changed files with 878 additions and 258 deletions
+3
View File
@@ -84,6 +84,9 @@ class ServiceItem
'section_uuid' => $this->section->getUuid(),
'staff_uuid' => $this->staff?->getUuid(),
'staff_name' => $this->staff?->getFullName(),
'staff' => $this->staff !== null
? ['uuid' => $this->staff->getUuid(), 'full_name' => $this->staff->getFullName()]
: null,
'name' => $this->name,
'price_rials' => $this->priceRials,
'active' => $this->active,
@@ -3,6 +3,7 @@
namespace App\Insurance\Controller;
use App\Auth\Entity\User;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Insurance\Entity\DoctorInsurance;
@@ -39,6 +40,7 @@ class InsuranceController extends BaseController
private readonly TenantInsuranceRepository $tenantInsuranceRepo,
private readonly TenantServiceCoverageRepository $serviceCoverageRepo,
private readonly TenantInsuranceService $tenantInsuranceService,
private readonly ServiceItemRepository $serviceItemRepo,
private readonly FileValidatorService $fileValidator,
private readonly string $projectDir,
) {}
@@ -403,7 +405,14 @@ class InsuranceController extends BaseController
$rows = $this->serviceCoverageRepo->findByContract($contract->getId());
return $this->success(['data' => array_map(fn($r) => $r->toArray(), $rows)]);
$data = array_map(function ($r) {
$row = $r->toArray();
$item = $this->serviceItemRepo->find($r->getServiceItemId());
$row['service_item_uuid'] = $item?->getUuid();
return $row;
}, $rows);
return $this->success(['data' => $data]);
}
#[Route('/api/v1/billing/tenant-insurances/{uuid}/service-coverage', methods: ['PUT'])]
@@ -416,12 +425,23 @@ class InsuranceController extends BaseController
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'قرارداد یافت نشد', 404);
}
$data = json_decode($request->getContent(), true) ?? [];
$serviceItemId = isset($data['service_item_id']) ? (int) $data['service_item_id'] : 0;
if ($serviceItemId <= 0) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'service_item_id الزامی است', 422);
$data = json_decode($request->getContent(), true) ?? [];
$serviceItem = isset($data['service_item_uuid'])
? $this->serviceItemRepo->findByUuid((string) $data['service_item_uuid'])
: (isset($data['service_item_id']) ? $this->serviceItemRepo->find((int) $data['service_item_id']) : null);
if ($serviceItem === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'سرویس یافت نشد', 422);
}
$section = $serviceItem->getSection();
if ($section->getEntityType() !== $entityType || $section->getEntityId() !== $entityId) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'این سرویس متعلق به شما نیست', 403);
}
$serviceItemId = $serviceItem->getId();
$this->tenantInsuranceService->setServiceCoverage(
$contract,
$serviceItemId,
+45 -9
View File
@@ -4,8 +4,11 @@ namespace App\Patient\Service;
use App\Appointment\Entity\Appointment;
use App\Auth\Repository\UserRepository;
use App\Billing\Service\BillingCalculator;
use App\Billing\ValueObject\Money;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Clinic\Repository\ClinicRepository;
use App\Insurance\Service\TenantInsuranceService;
use App\Doctor\Repository\DoctorAddressRepository;
use App\Patient\Entity\PatientRecord;
use App\Patient\Entity\PatientSession;
@@ -28,17 +31,46 @@ class PatientService
private readonly SubscriptionService $subscriptionService,
private readonly DoctorAddressRepository $addressRepo,
private readonly ClinicRepository $clinicRepo,
private readonly TenantInsuranceService $tenantInsuranceService,
private readonly BillingCalculator $billingCalculator,
) {}
public function calculateFinalPrice(int $visitPrice, float $baseDiscount, float $suppDiscount, array $serviceItems): array
{
$afterBase = $visitPrice * (1 - $baseDiscount / 100);
$afterSupp = $afterBase * (1 - $suppDiscount / 100);
$servicesTotal = array_sum(array_column($serviceItems, 'price_rials'));
/**
* محاسبه‌ی سهم بیمار.
* ویزیت با درصد تخفیف انتخاب‌شده در فرم؛ هر خدمت با قاعده‌ی پوشش همان بیمه‌گر برای همان خدمت
* (TenantServiceCoverage از طریق BillingCalculator). خدمتی که آن بیمه را پوشش نمی‌دهد، کامل بر عهده‌ی بیمار است.
*
* @param array<array{item_id: int, price_rials: int}> $serviceItems قیمت کل هر ردیف (با احتساب تعداد)
*/
public function calculateFinalPrice(
int $visitPrice,
float $baseDiscount,
float $suppDiscount,
array $serviceItems,
string $entityType = 'doctor',
int $entityId = 0,
?int $baseInsuranceId = null,
?int $suppInsuranceId = null,
): array {
$afterBase = $visitPrice * (1 - $baseDiscount / 100);
$afterSupp = $afterBase * (1 - $suppDiscount / 100);
$visitShare = (int) round($afterSupp);
$servicesTotal = 0;
$servicesPatient = 0;
foreach ($serviceItems as $svc) {
$servicesTotal += $svc['price_rials'];
$baseRule = $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $baseInsuranceId, $svc['item_id']);
$suppRule = $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $suppInsuranceId, $svc['item_id']);
$breakdown = $this->billingCalculator->calculateItem(new Money($svc['price_rials']), $baseRule, $suppRule);
$servicesPatient += $breakdown->patientRials;
}
return [
'services_total_rials' => (int) $servicesTotal,
'final_price_rials' => (int) round($afterSupp) + (int) $servicesTotal,
'services_total_rials' => $servicesTotal,
'final_price_rials' => $visitShare + $servicesPatient,
];
}
@@ -111,7 +143,7 @@ class PatientService
$item = $this->serviceItemRepo->findByUuid($svc['service_item_uuid'] ?? '');
if ($item !== null) {
$qty = max(1, (int) ($svc['quantity'] ?? 1));
$serviceItemsData[] = ['price_rials' => $item->getPriceRials() * $qty];
$serviceItemsData[] = ['item_id' => $item->getId(), 'price_rials' => $item->getPriceRials() * $qty];
}
}
@@ -119,7 +151,11 @@ class PatientService
$session->getVisitPriceRials(),
$session->getBaseInsuranceDiscountPercent(),
$session->getSupplementaryDiscountPercent(),
$serviceItemsData
$serviceItemsData,
$entityType,
$entityId,
$session->getInsuranceBaseId(),
$session->getInsuranceSupplementaryId(),
);
$session->setServicesTotalRials($priceCalc['services_total_rials']);