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,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Billing\Service;
|
||||
|
||||
use App\Billing\Contract\ClaimSubmitterInterface;
|
||||
use App\Billing\Entity\Claim;
|
||||
use App\Billing\Entity\ClaimItem;
|
||||
use App\Billing\Entity\Invoice;
|
||||
use App\Billing\Repository\ClaimRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Shared\Exception\AppException;
|
||||
|
||||
class ClaimService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClaimRepository $claimRepo,
|
||||
private readonly ClaimSubmitterInterface $submitter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* ساخت مطالبات از یک صورتحساب نهاییشده.
|
||||
* یک Claim برای بیمهی پایه و یک Claim برای بیمهی مکمل (در صورت وجود سهم).
|
||||
* @return Claim[]
|
||||
*/
|
||||
public function createFromInvoice(Invoice $invoice): array
|
||||
{
|
||||
if ($invoice->getStatus() !== Invoice::STATUS_FINALIZED) {
|
||||
throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'فقط از صورتحساب نهاییشده میتوان مطالبه ساخت', 422);
|
||||
}
|
||||
|
||||
$claims = [];
|
||||
|
||||
$baseId = $invoice->getBaseInsuranceId();
|
||||
if ($baseId !== null) {
|
||||
$claim = $this->buildClaim($invoice, $baseId, Claim::KIND_BASE);
|
||||
if ($claim !== null) {
|
||||
$claims[] = $claim;
|
||||
}
|
||||
}
|
||||
|
||||
$suppId = $invoice->getSupplementaryInsuranceId();
|
||||
if ($suppId !== null) {
|
||||
$claim = $this->buildClaim($invoice, $suppId, Claim::KIND_SUPPLEMENTARY);
|
||||
if ($claim !== null) {
|
||||
$claims[] = $claim;
|
||||
}
|
||||
}
|
||||
|
||||
if ($claims === []) {
|
||||
throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'سهم بیمهای برای این صورتحساب وجود ندارد', 422);
|
||||
}
|
||||
|
||||
foreach ($claims as $claim) {
|
||||
$this->claimRepo->save($claim, false);
|
||||
}
|
||||
$this->claimRepo->getEntityManager()->flush();
|
||||
|
||||
return $claims;
|
||||
}
|
||||
|
||||
private function buildClaim(Invoice $invoice, int $insuranceId, string $kind): ?Claim
|
||||
{
|
||||
$claim = new Claim($invoice->getEntityType(), $invoice->getEntityId(), $insuranceId, $kind);
|
||||
|
||||
$hasShare = false;
|
||||
foreach ($invoice->getItems() as $item) {
|
||||
$share = $kind === Claim::KIND_BASE
|
||||
? $item->getBaseInsuranceRials()
|
||||
: $item->getSupplementaryRials();
|
||||
|
||||
if ($share > 0) {
|
||||
$claim->addItem(new ClaimItem($claim, $item->getId(), $share));
|
||||
$hasShare = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $hasShare ? $claim : null;
|
||||
}
|
||||
|
||||
public function transition(Claim $claim, string $target, array $opts = []): void
|
||||
{
|
||||
if (!$claim->canTransitionTo($target)) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
sprintf('انتقال از «%s» به «%s» مجاز نیست', $claim->getStatus(), $target),
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
match ($target) {
|
||||
Claim::STATUS_SUBMITTED => $this->doSubmit($claim),
|
||||
Claim::STATUS_APPROVED => $claim->approve($opts['approved_rials'] ?? null),
|
||||
Claim::STATUS_REJECTED => $claim->reject($opts['reason'] ?? ''),
|
||||
Claim::STATUS_PAID => $claim->pay($opts['paid_rials'] ?? null),
|
||||
default => throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'وضعیت نامعتبر', 422),
|
||||
};
|
||||
|
||||
$this->claimRepo->save($claim);
|
||||
}
|
||||
|
||||
private function doSubmit(Claim $claim): void
|
||||
{
|
||||
$result = $this->submitter->submit($claim);
|
||||
if (!$result->success) {
|
||||
throw new AppException(ErrorCodes::ERR_VALIDATION_001, $result->errorMessage ?? 'ارسال مطالبه ناموفق بود', 422);
|
||||
}
|
||||
$claim->submit();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user