Files
clinicpro/tests/Insurance/TenantInsuranceContractApiTest.php
T
hamedandClaude Opus 4.8 d7cb9ea5a3 feat(insurance): redesign insurance management page to match Figma
Rebuild the /admin/insurance-pricing contracts UI to the Figma "مدیریت بیمه"
design and inject the coverage/franchise/ceiling fields the design omitted.

Backend:
- Add contract-level `kind` column to TenantInsurance (basic|supplementary),
  defaulting to the catalog type; migration Version20260715093358.
- POST/PATCH /billing/tenant-insurances now accept effective_from,
  effective_to, kind; PATCH also toggles is_active without clobbering the
  user-set effective_to (unlike DELETE/deactivate).
- List returns the latest version of every insurance (active + inactive) via
  TenantInsuranceRepository::findLatestByTenant, for the فعال/غیرفعال toggle.

Frontend:
- New InsuranceModal (ui/Modal + SearchableSelect + PersianDateInput) with the
  seven fields; submit "ثبت بیمه".
- TenantInsuranceContracts rebuilt: header + search box, desktop table
  (ردیف/نام/کد/نوع/وضعیت/عملیات) and mobile cards, status toggle -> PATCH.
- utils: isoToUnix/unixToIso helpers for contract dates.

Tests: TenantInsuranceContractApiTest (create/edit/toggle/list, 5 cases),
InsuranceModal + TenantInsuranceContracts vitest suites, docs/api updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:23:38 +03:30

132 lines
5.6 KiB
PHP

<?php
namespace App\Tests\Insurance;
use App\Doctor\Entity\Doctor;
use App\Insurance\Entity\Insurance;
use App\Insurance\Enum\InsuranceType;
use App\Tests\ApiTestCase;
/**
* Covers the tenant insurance contract management flow used by the redesigned
* /admin/insurance-pricing page: create with contract dates + kind, edit those
* fields, toggle فعال/غیرفعال, and list returning inactive contracts too.
*/
class TenantInsuranceContractApiTest extends ApiTestCase
{
/** @return array{0: \App\Auth\Entity\User, 1: Insurance} */
private function makeDoctorAndInsurance(InsuranceType $type = InsuranceType::Basic): array
{
$owner = $this->createUser(['ROLE_DOCTOR']);
$doctor = new Doctor($owner, 'دکتر تست بیمه');
$this->em->persist($doctor);
$insurance = new Insurance('بیمه ایران ' . random_int(1000, 9999), $type);
$this->em->persist($insurance);
$this->em->flush();
return [$owner, $insurance];
}
public function testCreateContractPersistsDatesAndKind(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance(InsuranceType::Basic);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 70,
'franchise_rials' => 500_000,
'annual_ceiling_rials' => 20_000_000,
'effective_from' => 1_700_000_000,
'effective_to' => 1_800_000_000,
'kind' => 'supplementary',
]);
$this->assertSame(201, $this->responseCode());
$row = $body['data']['data'];
$this->assertEquals(70.0, $row['coverage_percent']);
$this->assertSame(500_000, $row['franchise_rials']);
$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']);
$this->assertSame('supplementary', $row['kind']);
$this->assertTrue($row['is_active']);
}
public function testKindDefaultsToCatalogTypeWhenOmitted(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance(InsuranceType::Basic);
$body = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 50,
]);
$this->assertSame(201, $this->responseCode());
$this->assertSame('basic', $body['data']['data']['kind']);
}
public function testEditUpdatesDatesKindAndAmounts(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 40,
]);
$uuid = $created['data']['data']['uuid'];
$body = $this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, [
'coverage_percent' => 90,
'franchise_rials' => 123_000,
'annual_ceiling_rials' => null,
'effective_to' => 1_900_000_000,
'kind' => 'supplementary',
]);
$this->assertSame(200, $this->responseCode());
$this->assertEquals(90.0, $body['data']['data']['coverage_percent']);
$this->assertSame(123_000, $body['data']['data']['franchise_rials']);
$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']);
}
public function testToggleIsActiveDoesNotClobberEffectiveTo(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 60,
'effective_to' => 1_850_000_000,
]);
$uuid = $created['data']['data']['uuid'];
$off = $this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, ['is_active' => false]);
$this->assertFalse($off['data']['data']['is_active']);
// Deactivating via the toggle must keep the user-set effective_to intact.
$this->assertSame(1_850_000_000, $off['data']['data']['effective_to']);
$on = $this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, ['is_active' => true]);
$this->assertTrue($on['data']['data']['is_active']);
}
public function testListReturnsInactiveContracts(): void
{
[$owner, $insurance] = $this->makeDoctorAndInsurance();
$created = $this->authJson('POST', '/api/v1/billing/tenant-insurances', $owner, [
'insurance_id' => $insurance->getId(),
'coverage_percent' => 30,
]);
$uuid = $created['data']['data']['uuid'];
$this->authJson('PATCH', "/api/v1/billing/tenant-insurances/$uuid", $owner, ['is_active' => false]);
$body = $this->authJson('GET', '/api/v1/billing/tenant-insurances', $owner);
$this->assertSame(200, $this->responseCode());
$rows = $body['data']['data'] ?? $body['data'];
$found = array_filter($rows, fn ($r) => $r['uuid'] === $uuid);
$this->assertCount(1, $found, 'inactive contract must still appear in the list');
$this->assertFalse(array_values($found)[0]['is_active']);
}
}