Base insurance is a percentage-only rule: patient share is now total minus the base share, and the contract franchise no longer inflates it (franchise stays meaningful for supplementary contracts only). Coverage percentages are managed centrally by admin per service category (outpatient/inpatient, extensible via the ServiceCategory enum). A tenant contract may override a category, otherwise it follows the admin default live — changing the central value immediately applies to every contract that did not override it. - add ServiceCategory enum + GET /api/v1/service-categories as the single source of the category list for every client - add insurance_coverage_defaults (+ GET/PUT admin coverage-defaults endpoints) and expose coverage_defaults on the insurance list and insurance-pricing - add tenant_insurance_category_coverage; tenant-insurances accepts optional category_coverages (needs insurances.update) and returns the effective percentages with their source - add service_items.service_category; visits always resolve as outpatient - drop the reverse-engineered percent from patient_share_rials in MyPatientsPage and align the client-side BillingCalculator mirror in CreateStep Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
118 lines
4.8 KiB
PHP
118 lines
4.8 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Billing;
|
|
|
|
use App\Billing\Service\BillingCalculator;
|
|
use App\Billing\ValueObject\Money;
|
|
use App\Insurance\ValueObject\CoverageRule;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class BillingCalculatorTest extends TestCase
|
|
{
|
|
private BillingCalculator $calc;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->calc = new BillingCalculator();
|
|
}
|
|
|
|
public function testNoInsuranceAllPatient(): void
|
|
{
|
|
$b = $this->calc->calculateItem(new Money(600_000), null, null);
|
|
$this->assertSame(600_000, $b->patientRials);
|
|
$this->assertSame(0, $b->baseInsuranceRials);
|
|
$this->assertSame(0, $b->supplementaryRials);
|
|
}
|
|
|
|
public function testBaseAndSupplementary(): void
|
|
{
|
|
// کل 600,000؛ پایه 70% → 420,000؛ مکمل روی 180,000 با ~66.667% → 120,000؛ بیمار 60,000
|
|
$base = new CoverageRule(coveragePercent: 70, franchiseRials: 0, ceilingRials: null);
|
|
$supp = new CoverageRule(coveragePercent: 66.6667, franchiseRials: 0, ceilingRials: null);
|
|
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, $supp);
|
|
|
|
$this->assertSame(420_000, $b->baseInsuranceRials);
|
|
$this->assertSame(120_000, $b->supplementaryRials);
|
|
$this->assertSame(60_000, $b->patientRials);
|
|
$this->assertSame(
|
|
$b->totalRials,
|
|
$b->baseInsuranceRials + $b->supplementaryRials + $b->patientRials
|
|
);
|
|
}
|
|
|
|
public function testBaseOnly(): void
|
|
{
|
|
$base = new CoverageRule(coveragePercent: 70, franchiseRials: 0, ceilingRials: null);
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, null);
|
|
$this->assertSame(420_000, $b->baseInsuranceRials);
|
|
$this->assertSame(0, $b->supplementaryRials);
|
|
$this->assertSame(180_000, $b->patientRials);
|
|
}
|
|
|
|
public function testBaseCeilingCapsShare(): void
|
|
{
|
|
// پایه 70% = 420,000 ولی سقف 300,000 → بیمار باقی را میدهد
|
|
$base = new CoverageRule(coveragePercent: 70, franchiseRials: 0, ceilingRials: 300_000);
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, null);
|
|
$this->assertSame(300_000, $b->baseInsuranceRials);
|
|
$this->assertSame(300_000, $b->patientRials);
|
|
}
|
|
|
|
/** سناریوی مرجع کاربر: ویزیت 5,952,000 ریال با پوشش پایهٔ ۳۰٪ (بستری). */
|
|
public function testReferenceScenarioBasePercentOnly(): void
|
|
{
|
|
$base = new CoverageRule(coveragePercent: 30.0, franchiseRials: 0, ceilingRials: null);
|
|
|
|
$b = $this->calc->calculateItem(new Money(5_952_000), $base, null);
|
|
|
|
$this->assertSame(1_785_600, $b->baseInsuranceRials);
|
|
$this->assertSame(4_166_400, $b->patientRials);
|
|
$this->assertSame($b->totalRials, $b->baseInsuranceRials + $b->patientRials);
|
|
}
|
|
|
|
public function testBaseFranchiseDoesNotChargePatient(): void
|
|
{
|
|
// فرانشیز در بیمهٔ پایه بیاثر است: پایه 100% → سهم بیمار صفر.
|
|
$base = new CoverageRule(coveragePercent: 100, franchiseRials: 50_000, ceilingRials: null);
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, null);
|
|
$this->assertSame(0, $b->patientRials);
|
|
$this->assertSame(600_000, $b->baseInsuranceRials);
|
|
}
|
|
|
|
public function testSupplementaryFranchiseAddedToPatient(): void
|
|
{
|
|
$base = new CoverageRule(coveragePercent: 70, franchiseRials: 90_000, ceilingRials: null);
|
|
$supp = new CoverageRule(coveragePercent: 100, franchiseRials: 50_000, ceilingRials: null);
|
|
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, $supp);
|
|
|
|
$this->assertSame(420_000, $b->baseInsuranceRials);
|
|
$this->assertSame(180_000, $b->supplementaryRials);
|
|
$this->assertSame(50_000, $b->patientRials);
|
|
}
|
|
|
|
public function testZeroPercentLeavesEverythingToPatient(): void
|
|
{
|
|
$base = new CoverageRule(coveragePercent: 0, franchiseRials: 0, ceilingRials: null);
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, null);
|
|
$this->assertSame(0, $b->baseInsuranceRials);
|
|
$this->assertSame(600_000, $b->patientRials);
|
|
}
|
|
|
|
public function testFullPercentLeavesNothingToPatient(): void
|
|
{
|
|
$base = new CoverageRule(coveragePercent: 100, franchiseRials: 0, ceilingRials: null);
|
|
$b = $this->calc->calculateItem(new Money(600_000), $base, null);
|
|
$this->assertSame(600_000, $b->baseInsuranceRials);
|
|
$this->assertSame(0, $b->patientRials);
|
|
}
|
|
|
|
public function testNotCoveredRule(): void
|
|
{
|
|
$b = $this->calc->calculateItem(new Money(600_000), CoverageRule::notCovered(), CoverageRule::notCovered());
|
|
$this->assertSame(600_000, $b->patientRials);
|
|
$this->assertSame(0, $b->baseInsuranceRials);
|
|
}
|
|
}
|