- 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.
154 lines
6.9 KiB
PHP
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);
|
|
}
|
|
}
|