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>
167 lines
6.6 KiB
PHP
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);
|
|
}
|
|
}
|