Files
clinicpro/tests/Insurance/TenantInsuranceCategoryCoverageApiTest.php
T
hamedandClaude Opus 5 b24f45cc83 fix(insurance): read a doctor's own settings first, then their clinic's
A clinic owner configures insurance on the doctor (`doctor_uuid`), but an
appointment booked at the clinic belongs to the clinic — so at confirm time
the engine looked for contracts under the clinic, found none, and the operator
had no insurance to pick and no way to save one ("this insurance has no active
contract"). The two sides were writing and reading different tenants.

Contracts, service kinds and the visit price now resolve doctor-first with the
appointment's clinic as fallback, each judged separately: a doctor who holds
their own contracts but leaves the visit price to the clinic gets each from the
right place. The confirm modal asks the same question the engine answers, via
`inherit=1` on the two read endpoints; the settings pages deliberately do not
send it, since editing must target the doctor's own row.

Two further things came out of the same sweep. The service-kind settings
repository had the tenant-filter blindness already fixed for contracts and
pricing — reads pinned to the caller's environment while the target is another
tenant — so it is now exempted the same way. And a coverage percentage of zero
is accepted as a real choice meaning "this contract does not cover that service
kind"; what is still rejected is leaving an enabled kind with no percentage at
all, inheriting a central default of zero included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 17:18:35 +03:30

197 lines
8.3 KiB
PHP

<?php
namespace App\Tests\Insurance;
use App\Doctor\Entity\Doctor;
use App\Insurance\Entity\Insurance;
use App\Insurance\Enum\InsuranceType;
use App\Insurance\Service\InsuranceCoverageDefaultService;
use App\Tests\ApiTestCase;
/**
* Contract-level per-category coverage overrides over the tenant insurance API:
* sending them stores an override, omitting them keeps the contract on the central
* admin default (live fallback).
*/
class TenantInsuranceCategoryCoverageApiTest extends ApiTestCase
{
/** @return array{0: \App\Auth\Entity\User, 1: Insurance} */
private function makeDoctorAndInsurance(): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست پوشش نوع خدمت');
$this->em->persist($doctor);
$insurance = new Insurance('بیمه پایه ' . random_int(1000, 9999), InsuranceType::Basic);
$this->em->persist($insurance);
$this->em->flush();
return [$owner, $insurance];
}
public function testContractWithoutOverridesFollowsTheAdminDefault(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
static::getContainer()->get(InsuranceCoverageDefaultService::class)->save($insurance->getId(), [
['key' => 'outpatient', 'coverage_percent' => 70],
['key' => 'inpatient', 'coverage_percent' => 30],
]);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
]);
$this->assertSame(201, $this->responseCode());
$row = $body['data']['data'];
$this->assertEquals(70.0, $row['category_coverages']['outpatient']);
$this->assertEquals(30.0, $row['category_coverages']['inpatient']);
$this->assertSame('admin_default', $row['category_coverage_source']['outpatient']);
}
public function testSendingCategoryCoveragesStoresAnOverride(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
static::getContainer()->get(InsuranceCoverageDefaultService::class)->save($insurance->getId(), [
['key' => 'outpatient', 'coverage_percent' => 70],
['key' => 'inpatient', 'coverage_percent' => 30],
]);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [
['key' => 'inpatient', 'coverage_percent' => 90],
],
]);
$this->assertSame(201, $this->responseCode());
$row = $body['data']['data'];
$this->assertEquals(90.0, $row['category_coverages']['inpatient']);
$this->assertSame('override', $row['category_coverage_source']['inpatient']);
$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 testZeroIsAValidChosenPercentMeaningNoCoverage(): 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' => 0],
],
]);
$this->assertSame(201, $this->responseCode());
$this->assertEquals(0.0, $body['data']['data']['category_coverages']['inpatient']);
}
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();
static::getContainer()->get(InsuranceCoverageDefaultService::class)->save($insurance->getId(), [
['key' => 'outpatient', 'coverage_percent' => 55],
]);
$this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, ['insurance_id' => $insurance->getId()]);
$body = $this->authJson('GET', '/api/v1/billing/tenant-insurances', $owner);
$this->assertSame(200, $this->responseCode());
$row = $body['data']['data'][0];
$this->assertEquals(55.0, $row['category_coverages']['outpatient']);
$this->assertArrayHasKey('inpatient', $row['category_coverage_source']);
}
public function testUnknownCategoryIsRejected(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [['key' => 'dental', 'coverage_percent' => 50]],
]);
$this->assertSame(422, $this->responseCode());
}
public function testPercentAbove100IsRejected(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [['key' => 'outpatient', 'coverage_percent' => 101]],
]);
$this->assertSame(422, $this->responseCode());
}
public function testPatchCanDropAnOverrideBackToTheAdminDefault(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
// سرپایی هم پیش‌فرض دارد، وگرنه ساخت قرارداد به‌خاطر «درصد پوشش الزامی است» رد می‌شود.
static::getContainer()->get(InsuranceCoverageDefaultService::class)->save($insurance->getId(), [
['key' => 'outpatient', 'coverage_percent' => 50],
['key' => 'inpatient', 'coverage_percent' => 30],
]);
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'category_coverages' => [['key' => 'inpatient', 'coverage_percent' => 90]],
]);
$uuid = $created['data']['data']['uuid'];
$body = $this->authJson('PATCH', '/api/v1/billing/tenant-insurances/' . $uuid, $owner, [
'category_coverages' => [['key' => 'inpatient', 'coverage_percent' => null]],
]);
$this->assertSame(200, $this->responseCode());
$row = $body['data']['data'];
$this->assertEquals(30.0, $row['category_coverages']['inpatient']);
$this->assertSame('admin_default', $row['category_coverage_source']['inpatient']);
}
}