feat: Enhance insurance billing system to support supplementary insurance
- Updated CoverageRule and related entities to include franchise_percent instead of franchise_rials. - Modified Appointment entity to carry supplementary insurance ID alongside base insurance. - Implemented SessionBillingService to ensure finalized invoices for insured patient sessions. - Created InvoiceFinalized event to trigger claims creation upon invoice finalization. - Added BackfillMissingClaimsCommand to generate claims for finalized invoices without existing claims. - Developed tests to validate the new functionality for supplementary insurance handling in appointments and claims.
This commit is contained in:
@@ -1059,6 +1059,7 @@ class AppointmentController extends BaseController
|
||||
// تفکیک بیمه — مودالِ قطعیکردن همان مبلغی را نشان میدهد که ثبت شده.
|
||||
'insurance_service_category' => $session->getInsuranceServiceCategory()?->value,
|
||||
'insurance_base_id' => $session->getInsuranceBaseId(),
|
||||
'insurance_supplementary_id' => $session->getInsuranceSupplementaryId(),
|
||||
'gross_total_rials' => $session->getGrossTotalRials(),
|
||||
'base_insurance_rials' => $session->getBaseInsuranceRials(),
|
||||
'supplementary_insurance_rials' => $session->getSupplementaryInsuranceRials(),
|
||||
|
||||
@@ -195,6 +195,10 @@ class Appointment
|
||||
#[ORM\Column(name: 'insurance_base_id', type: 'integer', nullable: true)]
|
||||
private ?int $insuranceBaseId = null;
|
||||
|
||||
/** بیمهٔ تکمیلی روی باقیماندهٔ بعد از بیمهٔ پایه محاسبه میشود، نه روی کل مبلغ. */
|
||||
#[ORM\Column(name: 'insurance_supplementary_id', type: 'integer', nullable: true)]
|
||||
private ?int $insuranceSupplementaryId = null;
|
||||
|
||||
/**
|
||||
* Reserve-list entry (نوبت رزرو): booked for a day, not a time slot.
|
||||
* slotStart/slotEnd hold that day's midnight so date queries keep working.
|
||||
@@ -291,6 +295,7 @@ class Appointment
|
||||
public function getVisitPriceRials(): ?int { return $this->visitPriceRials; }
|
||||
public function getInsuranceServiceCategory(): ?ServiceCategory { return $this->insuranceServiceCategory; }
|
||||
public function getInsuranceBaseId(): ?int { return $this->insuranceBaseId; }
|
||||
public function getInsuranceSupplementaryId(): ?int { return $this->insuranceSupplementaryId; }
|
||||
public function isReserve(): bool { return $this->isReserve; }
|
||||
|
||||
public function setServiceSection(?\App\ClinicService\Entity\ServiceSection $v): self { $this->serviceSection = $v; return $this; }
|
||||
@@ -301,6 +306,7 @@ class Appointment
|
||||
public function setVisitPriceRials(?int $v): self { $this->visitPriceRials = $v; return $this; }
|
||||
public function setInsuranceServiceCategory(?ServiceCategory $v): self { $this->insuranceServiceCategory = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setInsuranceBaseId(?int $v): self { $this->insuranceBaseId = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setInsuranceSupplementaryId(?int $v): self { $this->insuranceSupplementaryId = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
/**
|
||||
* Move the appointment to a new slot (جا به جایی نوبت) and/or flip its
|
||||
@@ -422,6 +428,7 @@ class Appointment
|
||||
'insurance_service_category' => $this->insuranceServiceCategory?->value,
|
||||
'insurance_service_category_label' => $this->insuranceServiceCategory?->label(),
|
||||
'insurance_base_id' => $this->insuranceBaseId,
|
||||
'insurance_supplementary_id' => $this->insuranceSupplementaryId,
|
||||
'is_reserve' => $this->isReserve,
|
||||
'version' => $this->version,
|
||||
'created_at' => $this->createdAt,
|
||||
|
||||
@@ -73,7 +73,13 @@ class AppointmentInsuranceService
|
||||
|
||||
if (array_key_exists('insurance_base_id', $data)) {
|
||||
$appointment->setInsuranceBaseId(
|
||||
$this->resolveBaseInsuranceId($data['insurance_base_id'], $entityType, $entityId)
|
||||
$this->resolveInsuranceId($data['insurance_base_id'], $entityType, $entityId, false)
|
||||
);
|
||||
}
|
||||
|
||||
if (array_key_exists('insurance_supplementary_id', $data)) {
|
||||
$appointment->setInsuranceSupplementaryId(
|
||||
$this->resolveInsuranceId($data['insurance_supplementary_id'], $entityType, $entityId, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -106,8 +112,15 @@ class AppointmentInsuranceService
|
||||
return $category;
|
||||
}
|
||||
|
||||
private function resolveBaseInsuranceId(mixed $raw, string $entityType, int $entityId): ?int
|
||||
/**
|
||||
* شناسهٔ بیمهٔ معتبر برای این نوبت، یا null وقتی انتخاب پاک شده است.
|
||||
* قرارداد باید فعال باشد و نوعش با جایگاهی که در آن انتخاب شده بخواند —
|
||||
* بیمهٔ تکمیلی نمیتواند جای بیمهٔ پایه بنشیند و برعکس.
|
||||
*/
|
||||
private function resolveInsuranceId(mixed $raw, string $entityType, int $entityId, bool $supplementary): ?int
|
||||
{
|
||||
$field = $supplementary ? 'insurance_supplementary_id' : 'insurance_base_id';
|
||||
|
||||
if ($raw === null || $raw === '' || (int) $raw <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -119,18 +132,20 @@ class AppointmentInsuranceService
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
'این بیمه برای این پزشک/کلینیک قرارداد فعال ندارد',
|
||||
422,
|
||||
'insurance_base_id',
|
||||
$field,
|
||||
);
|
||||
}
|
||||
|
||||
// نوعِ قرارداد بر نوع کاتالوگ اولویت دارد — همان قاعدهٔ TenantInsuranceService.
|
||||
$kind = $contract->getKind() ?? $this->insuranceRepo->find($insuranceId)?->getType()->value;
|
||||
if ($kind === InsuranceType::Supplementary->value) {
|
||||
$kind = $contract->getKind() ?? $this->insuranceRepo->find($insuranceId)?->getType()->value;
|
||||
$isSupp = $kind === InsuranceType::Supplementary->value;
|
||||
|
||||
if ($isSupp !== $supplementary) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
'برای نوبت فقط بیمهٔ پایه قابل انتخاب است',
|
||||
$supplementary ? 'اینجا فقط بیمهٔ تکمیلی قابل انتخاب است' : 'اینجا فقط بیمهٔ پایه قابل انتخاب است',
|
||||
422,
|
||||
'insurance_base_id',
|
||||
$field,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user