Files
clinicpro/tests/Patient/AppointmentSupplementaryInsuranceTest.php
hamed e6267080b2 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.
2026-07-29 19:57:02 +03:30

154 lines
6.9 KiB
PHP

<?php
namespace App\Tests\Patient;
use App\Appointment\Entity\Appointment;
use App\Appointment\Service\AppointmentInsuranceService;
use App\Billing\Repository\ClaimRepository;
use App\Doctor\Entity\Doctor;
use App\Insurance\Entity\Insurance;
use App\Insurance\Entity\TenantInsurance;
use App\Insurance\Enum\InsuranceType;
use App\Insurance\Enum\ServiceCategory;
use App\Insurance\Service\TenantInsuranceService;
use App\Patient\Service\PatientService;
use App\Tests\ApiTestCase;
/**
* بیمهٔ تکمیلی هم مثل پایه روی خودِ نوبت انتخاب می‌شود، و در مراجعه‌ای که از قطعی‌کردن
* ساخته می‌شود زنجیره‌ای اعمال می‌گردد: اول پایه روی کل، بعد تکمیلی روی باقیمانده.
*/
class AppointmentSupplementaryInsuranceTest extends ApiTestCase
{
private Doctor $doctor;
private Insurance $basic;
private Insurance $supplementary;
protected function setUp(): void
{
parent::setUp();
$this->doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر بیمه تکمیلی');
$this->em->persist($this->doctor);
$this->basic = new Insurance('پایهٔ تست ' . random_int(1000, 9999), InsuranceType::Basic);
$this->supplementary = new Insurance('تکمیلی تست ' . random_int(1000, 9999), InsuranceType::Supplementary);
$this->em->persist($this->basic);
$this->em->persist($this->supplementary);
$this->em->flush();
}
private function contract(Insurance $insurance, float $percent, float $franchise = 0.0): TenantInsurance
{
$service = static::getContainer()->get(TenantInsuranceService::class);
$contract = $service->activate(
TenantInsurance::TYPE_DOCTOR,
(int) $this->doctor->getId(),
(int) $insurance->getId(),
$percent,
$franchise,
);
$service->setCategoryCoverages($contract, [
['key' => 'outpatient', 'coverage_percent' => $percent],
['key' => 'inpatient', 'coverage_percent' => $percent],
]);
return $contract;
}
private function appointmentWithVisitPrice(int $rials): Appointment
{
$appointment = $this->newAppointment($this->doctor, $this->createUser(['ROLE_USER']), 1_790_000_000, 1_790_001_800);
$appointment->setVisitPriceRials($rials);
$appointment->setInsuranceServiceCategory(ServiceCategory::Outpatient);
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
$this->em->persist($appointment);
$this->em->flush();
return $appointment;
}
public function testTheSessionBillsBaseThenSupplementaryOnTheRemainder(): void
{
$this->contract($this->basic, 30.0);
$this->contract($this->supplementary, 90.0, 10.0);
$appointment = $this->appointmentWithVisitPrice(10_000_000);
static::getContainer()->get(AppointmentInsuranceService::class)->apply($appointment, [
'insurance_base_id' => $this->basic->getId(),
'insurance_supplementary_id' => $this->supplementary->getId(),
]);
$this->em->flush();
$session = static::getContainer()->get(PatientService::class)->autoCreateOnAppointmentConfirm($appointment);
// پایه ۳۰٪ از ۱۰,۰۰۰,۰۰۰ → ۳,۰۰۰,۰۰۰؛ باقیمانده ۷,۰۰۰,۰۰۰؛
// تکمیلی ۹۰٪ منهای فرانشیز ۱۰٪ → ۵,۶۰۰,۰۰۰؛ سهم بیمار ۱,۴۰۰,۰۰۰.
self::assertSame((int) $this->supplementary->getId(), $session->getInsuranceSupplementaryId());
self::assertSame(3_000_000, $session->getBaseInsuranceRials());
self::assertSame(5_600_000, $session->getSupplementaryInsuranceRials());
self::assertSame(1_400_000, $session->getPatientShareRials());
self::assertSame(
$session->getGrossTotalRials(),
$session->getBaseInsuranceRials() + $session->getSupplementaryInsuranceRials() + $session->getPatientShareRials(),
);
}
public function testASupplementaryInsuranceCannotBePickedAsTheBasicOne(): void
{
$this->contract($this->supplementary, 90.0);
$appointment = $this->appointmentWithVisitPrice(1_000_000);
$this->expectExceptionMessage('اینجا فقط بیمهٔ پایه قابل انتخاب است');
static::getContainer()->get(AppointmentInsuranceService::class)->apply($appointment, [
'insurance_base_id' => $this->supplementary->getId(),
]);
}
public function testABasicInsuranceCannotBePickedAsTheSupplementaryOne(): void
{
$this->contract($this->basic, 30.0);
$appointment = $this->appointmentWithVisitPrice(1_000_000);
$this->expectExceptionMessage('اینجا فقط بیمهٔ تکمیلی قابل انتخاب است');
static::getContainer()->get(AppointmentInsuranceService::class)->apply($appointment, [
'insurance_supplementary_id' => $this->basic->getId(),
]);
}
public function testAnInsuranceWithoutAnActiveContractIsRefused(): void
{
$appointment = $this->appointmentWithVisitPrice(1_000_000);
$this->expectExceptionMessage('این بیمه برای این پزشک/کلینیک قرارداد فعال ندارد');
static::getContainer()->get(AppointmentInsuranceService::class)->apply($appointment, [
'insurance_supplementary_id' => $this->supplementary->getId(),
]);
}
/** مراجعهٔ تکمیلی‌دار باید مطالبهٔ تکمیلی هم بگیرد، نه فقط پایه. */
public function testTheConfirmedVisitProducesBothClaims(): void
{
$this->contract($this->basic, 30.0);
$this->contract($this->supplementary, 90.0, 10.0);
$appointment = $this->appointmentWithVisitPrice(10_000_000);
static::getContainer()->get(AppointmentInsuranceService::class)->apply($appointment, [
'insurance_base_id' => $this->basic->getId(),
'insurance_supplementary_id' => $this->supplementary->getId(),
]);
$this->em->flush();
// قطعی‌کردن به‌تنهایی باید صورتحساب و مطالبه بسازد — بدون هیچ گام دستی.
$session = static::getContainer()->get(PatientService::class)->autoCreateOnAppointmentConfirm($appointment);
$invoice = static::getContainer()->get(\App\Billing\Repository\InvoiceRepository::class)
->findBySession((int) $session->getId());
self::assertNotNull($invoice, 'مراجعهٔ بیمه‌دارِ آمده از نوبت باید صورتحساب داشته باشد');
$kinds = static::getContainer()->get(ClaimRepository::class)->kindsForInvoice((int) $invoice->getId());
sort($kinds);
self::assertSame(['base', 'supplementary'], $kinds);
}
}