Files
clinicpro/src/Insurance/Service/TenantInsuranceService.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

167 lines
6.6 KiB
PHP

<?php
namespace App\Insurance\Service;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Insurance\Entity\TenantInsurance;
use App\Insurance\Repository\InsuranceRepository;
use App\Insurance\Repository\TenantInsuranceRepository;
use App\Insurance\Repository\TenantServiceCoverageRepository;
use App\Insurance\ValueObject\CoverageRule;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
class TenantInsuranceService
{
public function __construct(
private readonly TenantInsuranceRepository $repo,
private readonly InsuranceRepository $insuranceRepo,
private readonly TenantServiceCoverageRepository $coverageRepo,
private readonly ServiceItemRepository $serviceItemRepo,
) {}
/**
* فعال‌سازی یا به‌روزرسانی قرارداد بیمه برای یک tenant.
* اگر قرارداد فعالی موجود باشد، همان ویرایش می‌شود؛ در غیر این صورت نسخه‌ی جدید ساخته می‌شود.
*/
public function activate(
string $entityType,
int $entityId,
int $insuranceId,
float $coveragePercent,
int $franchiseRials = 0,
?int $annualCeilingRials = null,
?int $effectiveFrom = null,
?int $effectiveTo = null,
?string $kind = null,
): TenantInsurance {
$insurance = $this->insuranceRepo->find($insuranceId);
if ($insurance === null) {
throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'بیمه یافت نشد', 404);
}
$contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
if ($contract === null) {
$version = $this->repo->latestVersion($entityType, $entityId, $insuranceId) + 1;
$contract = new TenantInsurance($entityType, $entityId, $insuranceId, $version);
}
$contract->setCoveragePercent($coveragePercent)
->setFranchiseRials($franchiseRials)
->setAnnualCeilingRials($annualCeilingRials)
// kind defaults to the catalog type; caller may override to categorise the contract.
->setKind($kind ?? $insurance->getType()->value)
->setEffectiveTo($effectiveTo)
->setActive(true);
if ($effectiveFrom !== null) {
$contract->setEffectiveFrom($effectiveFrom);
}
$this->repo->save($contract);
return $contract;
}
public function deactivate(TenantInsurance $contract): void
{
$contract->setActive(false)->setEffectiveTo(time());
$this->repo->save($contract);
}
/**
* بررسی فعال‌بودن یک بیمه برای tenant. در پذیرش/صورتحساب استفاده می‌شود.
*/
public function assertActive(string $entityType, int $entityId, int $insuranceId): TenantInsurance
{
$contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
if ($contract === null) {
throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'این بیمه برای این کلینیک/پزشک فعال نیست', 422);
}
return $contract;
}
/**
* قانون پوشش یک بیمه برای tenant جاری (برای BillingCalculator).
* اگر قرارداد فعالی نباشد، notCovered برمی‌گردد.
*/
public function coverageRule(string $entityType, int $entityId, ?int $insuranceId): CoverageRule
{
if ($insuranceId === null) {
return CoverageRule::notCovered();
}
$contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
if ($contract === null) {
return CoverageRule::notCovered();
}
return new CoverageRule(
coveragePercent: $contract->getCoveragePercent(),
franchiseRials: $contract->getFranchiseRials(),
ceilingRials: $contract->getAnnualCeilingRials(),
covered: true,
);
}
/**
* قانون پوشش یک خدمت خاص تحت بیمه‌ی tenant.
* اگر override خدمت موجود باشد اعمال می‌شود؛ فیلدهای null از قرارداد ارث می‌برند.
*/
public function coverageRuleForService(string $entityType, int $entityId, ?int $insuranceId, int $serviceItemId): CoverageRule
{
if ($insuranceId === null) {
return CoverageRule::notCovered();
}
// پرچم سرویس gate نهایی است: اگر «این خدمت شامل بیمه می‌شود» غیرفعال باشد، هیچ پوششی محاسبه نمی‌شود.
$service = $this->serviceItemRepo->find($serviceItemId);
if ($service === null || !$service->isInsuranceCovered()) {
return CoverageRule::notCovered();
}
$contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
if ($contract === null) {
return CoverageRule::notCovered();
}
$override = $this->coverageRepo->findOneFor($contract->getId(), $serviceItemId);
if ($override !== null && !$override->isCovered()) {
return CoverageRule::notCovered();
}
return new CoverageRule(
coveragePercent: $override?->getCoveragePercent() ?? $contract->getCoveragePercent(),
franchiseRials: $override?->getFranchiseRials() ?? $contract->getFranchiseRials(),
ceilingRials: $override?->getCeilingRials() ?? $contract->getAnnualCeilingRials(),
covered: true,
);
}
/** @return array{covered: bool, percent: float|null, franchise: int|null, ceiling: int|null}|null */
public function getServiceCoverage(int $tenantInsuranceId, int $serviceItemId): ?array
{
$override = $this->coverageRepo->findOneFor($tenantInsuranceId, $serviceItemId);
return $override?->toArray();
}
public function setServiceCoverage(
TenantInsurance $contract,
int $serviceItemId,
bool $covered,
?float $coveragePercent,
?int $franchiseRials,
?int $ceilingRials,
): void {
$override = $this->coverageRepo->findOneFor($contract->getId(), $serviceItemId)
?? new \App\Insurance\Entity\TenantServiceCoverage($contract->getId(), $serviceItemId);
$override->setCovered($covered)
->setCoveragePercent($coveragePercent)
->setFranchiseRials($franchiseRials)
->setCeilingRials($ceilingRials);
$this->coverageRepo->save($override);
}
}