feat: insurance & medical billing system (6 phases)
Multi-tenant insurance contracts, service coverage, versioned tariffs, invoice calculation, and insurance claims with debt reporting. - TenantInsurance: per-tenant insurance contracts (coverage/franchise/ceiling, versioning, soft-deactivate) + active guard - ServiceItem.insuranceCovered + TenantServiceCoverage per-service overrides - Tariff: versioned yearly tariffs with fallback to ServiceItem price - Billing domain: Money/ShareBreakdown VOs, BillingCalculator (unit-tested), Invoice/InvoiceItem aggregate, InvoiceService.createFromSession - Claim/ClaimItem with state machine (pending->submitted->approved/rejected->paid), ClaimService, insurance-debt report - ClaimSubmitterInterface + ManualClaimSubmitter (future insurance API ready) - Admin UI: insurance-pricing page, claims page, service tariff modal, service insurance toggle; routes + sidebar entries - Architecture doc + billing/insurance/clinic-services API docs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Insurance\Service;
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* فعالسازی یا بهروزرسانی قرارداد بیمه برای یک tenant.
|
||||
* اگر قرارداد فعالی موجود باشد، همان ویرایش میشود؛ در غیر این صورت نسخهی جدید ساخته میشود.
|
||||
*/
|
||||
public function activate(
|
||||
string $entityType,
|
||||
int $entityId,
|
||||
int $insuranceId,
|
||||
float $coveragePercent,
|
||||
int $franchiseRials = 0,
|
||||
?int $annualCeilingRials = null,
|
||||
): TenantInsurance {
|
||||
if ($this->insuranceRepo->find($insuranceId) === 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)
|
||||
->setActive(true);
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user