- Consolidated the calculation of patient and insurance shares into a single method using BillingCalculator. - Introduced new fields in PatientSession to store breakdown of insurance shares and patient share. - Updated the API responses to include the new fields for consistency across payment, invoice, and claims dashboard. - Added migration to backfill existing sessions with appropriate values for the new fields. - Implemented tests to ensure the correctness of the new logic and verify that the breakdown sums to the gross total. - Redesigned the claims dashboard to provide a more user-friendly overview of patient claims and their statuses.
154 lines
6.2 KiB
PHP
154 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Patient;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\ClinicService\Entity\ServiceSection;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Insurance\Entity\Insurance;
|
|
use App\Insurance\Entity\TenantInsurance;
|
|
use App\Insurance\Entity\TenantServiceCoverage;
|
|
use App\Insurance\Enum\InsuranceType;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* POST /api/v1/patient/{uuid}/session — the insurance share breakdown persisted on
|
|
* the session. The payment page, the invoice modal and the claims dashboard all read
|
|
* these fields, so they must come out of BillingCalculator and never be re-derived.
|
|
*/
|
|
class SessionInsuranceShareTest extends ApiTestCase
|
|
{
|
|
/** @return array{0: User, 1: Doctor, 2: PatientRecord} */
|
|
private function tenant(): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
|
$doctor = new Doctor($owner, 'دکتر تست');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
$patient = $this->createUser(['ROLE_USER']);
|
|
$record = new PatientRecord('doctor', $doctor->getId(), $patient, 'doctor', $doctor->getId());
|
|
$this->em->persist($record);
|
|
$this->em->flush();
|
|
|
|
return [$owner, $doctor, $record];
|
|
}
|
|
|
|
private function serviceItem(Doctor $doctor, int $priceRials, bool $insuranceCovered): ServiceItem
|
|
{
|
|
$section = new ServiceSection('doctor', $doctor->getId(), 'جراحی');
|
|
$this->em->persist($section);
|
|
|
|
$item = new ServiceItem($section, 'جراحی بینی', $priceRials);
|
|
$item->setInsuranceCovered($insuranceCovered);
|
|
$this->em->persist($item);
|
|
$this->em->flush();
|
|
|
|
return $item;
|
|
}
|
|
|
|
/** Active contract covering $coveragePercent of every covered service. */
|
|
private function contract(Doctor $doctor, float $coveragePercent, ?ServiceItem $item = null): Insurance
|
|
{
|
|
$insurance = new Insurance('تامین اجتماعی', InsuranceType::Basic);
|
|
$this->em->persist($insurance);
|
|
$this->em->flush();
|
|
|
|
$contract = new TenantInsurance('doctor', $doctor->getId(), $insurance->getId());
|
|
$contract->setCoveragePercent($coveragePercent)->setActive(true);
|
|
$this->em->persist($contract);
|
|
$this->em->flush();
|
|
|
|
if ($item !== null) {
|
|
$coverage = new TenantServiceCoverage($contract->getId(), $item->getId());
|
|
$coverage->setCovered(true)->setCoveragePercent($coveragePercent);
|
|
$this->em->persist($coverage);
|
|
$this->em->flush();
|
|
}
|
|
|
|
return $insurance;
|
|
}
|
|
|
|
public function testSessionStoresInsuranceAndPatientSharesForACoveredService(): void
|
|
{
|
|
[$owner, $doctor, $record] = $this->tenant();
|
|
$item = $this->serviceItem($doctor, 40_000_000, true);
|
|
$insurance = $this->contract($doctor, 70.0, $item);
|
|
|
|
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
|
|
'visit_price_rials' => 3_000_000,
|
|
'insurance_base_id' => $insurance->getId(),
|
|
'services' => [['service_item_uuid' => $item->getUuid(), 'quantity' => 1]],
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$session = $res['data'];
|
|
// 70% of both the visit and the service is carried by the insurer.
|
|
self::assertSame(43_000_000, $session['gross_total_rials']);
|
|
self::assertSame(30_100_000, $session['base_insurance_rials']);
|
|
self::assertSame(0, $session['supplementary_insurance_rials']);
|
|
self::assertSame(12_900_000, $session['patient_share_rials']);
|
|
// The payable amount is the patient share, not the gross total.
|
|
self::assertSame(12_900_000, $session['final_price_rials']);
|
|
self::assertSame(12_900_000, $session['remaining_rials']);
|
|
}
|
|
|
|
public function testTheBreakdownAlwaysSumsBackToTheGrossTotal(): void
|
|
{
|
|
[$owner, $doctor, $record] = $this->tenant();
|
|
$item = $this->serviceItem($doctor, 1_234_567, true);
|
|
$insurance = $this->contract($doctor, 33.33, $item);
|
|
|
|
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
|
|
'visit_price_rials' => 987_654,
|
|
'insurance_base_id' => $insurance->getId(),
|
|
'services' => [['service_item_uuid' => $item->getUuid(), 'quantity' => 3]],
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$s = $res['data'];
|
|
self::assertSame(
|
|
$s['gross_total_rials'],
|
|
$s['base_insurance_rials'] + $s['supplementary_insurance_rials'] + $s['patient_share_rials'],
|
|
);
|
|
}
|
|
|
|
public function testAServiceWithoutCoverageLeavesTheWholeAmountToThePatient(): void
|
|
{
|
|
[$owner, $doctor, $record] = $this->tenant();
|
|
$item = $this->serviceItem($doctor, 40_000_000, false);
|
|
$insurance = $this->contract($doctor, 70.0);
|
|
|
|
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
|
|
'visit_price_rials' => 0,
|
|
'insurance_base_id' => $insurance->getId(),
|
|
'services' => [['service_item_uuid' => $item->getUuid(), 'quantity' => 1]],
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$s = $res['data'];
|
|
self::assertSame(0, $s['base_insurance_rials']);
|
|
self::assertSame(40_000_000, $s['patient_share_rials']);
|
|
self::assertSame(40_000_000, $s['final_price_rials']);
|
|
}
|
|
|
|
public function testSessionWithoutInsuranceKeepsTheFullAmountAsThePatientShare(): void
|
|
{
|
|
[$owner, $doctor, $record] = $this->tenant();
|
|
$item = $this->serviceItem($doctor, 2_400_000, true);
|
|
|
|
$res = $this->authJson('POST', '/api/v1/patient/' . $record->getUuid() . '/session', $owner, [
|
|
'visit_price_rials' => 500_000,
|
|
'services' => [['service_item_uuid' => $item->getUuid(), 'quantity' => 1]],
|
|
]);
|
|
self::assertSame(201, $this->responseCode());
|
|
|
|
$s = $res['data'];
|
|
self::assertSame(2_900_000, $s['gross_total_rials']);
|
|
self::assertSame(0, $s['base_insurance_rials']);
|
|
self::assertSame(2_900_000, $s['patient_share_rials']);
|
|
}
|
|
}
|