feat(migrations): update franchise to percentage in tenant_insurances and tenant_service_coverage

- Changed franchise_rials to franchise_percent in tenant_insurances and tenant_service_coverage tables.
- Reset old rial values to 0/NULL as they are not convertible to percentage.

feat(command): add SeedInsuranceScenarioCommand for seeding insurance data

- Implemented a command to seed supplementary insurance contracts, patients, and claims for a specified doctor.
- Includes functionality for purging existing scenario data and generating new entries with predefined contracts and patient scenarios.
This commit is contained in:
hamed
2026-07-29 13:28:59 +03:30
parent 11b4dcdd34
commit 4f4bce9fe2
31 changed files with 1497 additions and 137 deletions
@@ -107,7 +107,11 @@ class CoveragePercentResolutionTest extends ApiTestCase
public function testServiceOverrideWinsOverEveryOtherLevel(): void
{
[$contract, $insurance, $item] = $this->makeContract(55.0);
$this->defaults->save($insurance->getId(), [['key' => 'inpatient', 'coverage_percent' => 30]]);
// سرپایی هم پیش‌فرض دارد، وگرنه ذخیرهٔ درصدها با «درصد پوشش الزامی است» رد می‌شود.
$this->defaults->save($insurance->getId(), [
['key' => 'inpatient', 'coverage_percent' => 30],
['key' => 'outpatient', 'coverage_percent' => 70],
]);
$this->service->setCategoryCoverages($contract, [['key' => 'inpatient', 'coverage_percent' => 90]]);
$this->service->setServiceCoverage($contract, $item->getId(), true, 25.0, null, null);
@@ -139,7 +143,7 @@ class CoveragePercentResolutionTest extends ApiTestCase
);
$this->assertSame(30.0, $rule->coveragePercent);
$this->assertSame(0, $rule->franchiseRials, 'فرانشیز در بیمهٔ پایه صفر می‌ماند');
$this->assertSame(0.0, $rule->franchisePercent, 'فرانشیز در بیمهٔ پایه صفر می‌ماند');
}
/** ویزیت خدمتِ سرپایی است. */
@@ -163,7 +167,10 @@ class CoveragePercentResolutionTest extends ApiTestCase
public function testRemovingTheOverrideReturnsToTheAdminDefault(): void
{
[$contract, $insurance] = $this->makeContract(55.0);
$this->defaults->save($insurance->getId(), [['key' => 'inpatient', 'coverage_percent' => 30]]);
$this->defaults->save($insurance->getId(), [
['key' => 'inpatient', 'coverage_percent' => 30],
['key' => 'outpatient', 'coverage_percent' => 70],
]);
$this->service->setCategoryCoverages($contract, [['key' => 'inpatient', 'coverage_percent' => 90]]);
$this->service->setCategoryCoverages($contract, [['key' => 'inpatient', 'coverage_percent' => null]]);
@@ -70,6 +70,45 @@ class TenantInsuranceCategoryCoverageApiTest extends ApiTestCase
$this->assertSame('admin_default', $row['category_coverage_source']['outpatient']);
}
public function testAnEnabledServiceKindCannotBeLeftWithoutAPercent(): void
{
// بدون پیش‌فرض مرکزی: ارسال درصد فقط برای سرپایی، بستری را صفر رها می‌کند.
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [
['key' => 'outpatient', 'coverage_percent' => 70],
['key' => 'inpatient', 'coverage_percent' => null],
],
]);
$this->assertSame(422, $this->responseCode());
$this->assertSame('category_coverages', $body['errors'][0]['field']);
$this->assertStringContainsString('خدمات بستری', $body['errors'][0]['message']);
}
public function testADisabledServiceKindNeedsNoPercent(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$this->authJson('PUT', '/api/v1/insurance-pricing', $owner, [
'service_categories' => [
['key' => 'outpatient', 'enabled' => true],
['key' => 'inpatient', 'enabled' => false],
],
]);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [
['key' => 'outpatient', 'coverage_percent' => 70],
],
]);
$this->assertSame(201, $this->responseCode());
$this->assertEquals(70.0, $body['data']['data']['category_coverages']['outpatient']);
}
public function testListReturnsEffectivePercentsAndSources(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
@@ -113,8 +152,10 @@ class TenantInsuranceCategoryCoverageApiTest extends ApiTestCase
public function testPatchCanDropAnOverrideBackToTheAdminDefault(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
// سرپایی هم پیش‌فرض دارد، وگرنه ساخت قرارداد به‌خاطر «درصد پوشش الزامی است» رد می‌شود.
static::getContainer()->get(InsuranceCoverageDefaultService::class)->save($insurance->getId(), [
['key' => 'inpatient', 'coverage_percent' => 30],
['key' => 'outpatient', 'coverage_percent' => 50],
['key' => 'inpatient', 'coverage_percent' => 30],
]);
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
@@ -35,7 +35,7 @@ class TenantInsuranceContractApiTest extends ApiTestCase
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 70,
'franchise_rials' => 500_000,
'franchise_percent' => 15,
'annual_ceiling_rials' => 20_000_000,
'effective_from' => 1_700_000_000,
'effective_to' => 1_800_000_000,
@@ -45,7 +45,7 @@ class TenantInsuranceContractApiTest extends ApiTestCase
$this->assertSame(201, $this->responseCode());
$row = $body['data']['data'];
$this->assertEquals(70.0, $row['coverage_percent']);
$this->assertSame(500_000, $row['franchise_rials']);
$this->assertEquals(15.0, $row['franchise_percent']);
$this->assertSame(20_000_000, $row['annual_ceiling_rials']);
$this->assertSame(1_700_000_000, $row['effective_from']);
$this->assertSame(1_800_000_000, $row['effective_to']);
@@ -53,6 +53,37 @@ class TenantInsuranceContractApiTest extends ApiTestCase
$this->assertTrue($row['is_active']);
}
public function testFranchiseAboveHundredIsRejected(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance(InsuranceType::Supplementary);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 80,
'franchise_percent' => 150,
]);
$this->assertSame(422, $this->responseCode());
$this->assertSame('franchise_percent', $body['errors'][0]['field']);
}
public function testNegativeFranchiseOnEditIsRejected(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance(InsuranceType::Supplementary);
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 80,
]);
$uuid = $created['data']['data']['uuid'];
$body = $this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, [
'franchise_percent' => -5,
]);
$this->assertSame(422, $this->responseCode());
$this->assertSame('franchise_percent', $body['errors'][0]['field']);
}
public function testKindDefaultsToCatalogTypeWhenOmitted(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance(InsuranceType::Basic);
@@ -77,7 +108,7 @@ class TenantInsuranceContractApiTest extends ApiTestCase
$body = $this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, [
'coverage_percent' => 90,
'franchise_rials' => 123_000,
'franchise_percent' => 20,
'annual_ceiling_rials' => null,
'effective_to' => 1_900_000_000,
'kind' => 'supplementary',
@@ -85,7 +116,7 @@ class TenantInsuranceContractApiTest extends ApiTestCase
$this->assertSame(200, $this->responseCode());
$this->assertEquals(90.0, $body['data']['data']['coverage_percent']);
$this->assertSame(123_000, $body['data']['data']['franchise_rials']);
$this->assertEquals(20.0, $body['data']['data']['franchise_percent']);
$this->assertNull($body['data']['data']['annual_ceiling_rials']);
$this->assertSame(1_900_000_000, $body['data']['data']['effective_to']);
$this->assertSame('supplementary', $body['data']['data']['kind']);