feat: Enhance insurance billing system to support supplementary insurance

- Updated CoverageRule and related entities to include franchise_percent instead of franchise_rials.
- Modified Appointment entity to carry supplementary insurance ID alongside base insurance.
- Implemented SessionBillingService to ensure finalized invoices for insured patient sessions.
- Created InvoiceFinalized event to trigger claims creation upon invoice finalization.
- Added BackfillMissingClaimsCommand to generate claims for finalized invoices without existing claims.
- Developed tests to validate the new functionality for supplementary insurance handling in appointments and claims.
This commit is contained in:
hamed
2026-07-29 19:57:02 +03:30
parent 9b05c6d1ff
commit e6267080b2
25 changed files with 876 additions and 126 deletions
@@ -390,6 +390,26 @@ class ClaimRepository extends ServiceEntityRepository
return $count > 0;
}
/**
* نوع مطالبه‌هایی که برای این صورتحساب از قبل ثبت شده‌اند (`base` / `supplementary`).
* مبنای idempotent بودنِ ساخت خودکار مطالبه است.
*
* @return list<string>
*/
public function kindsForInvoice(int $invoiceId): array
{
$rows = $this->createQueryBuilder('c')
->select('DISTINCT c.insuranceKind AS kind')
->join('c.items', 'ci')
->join('App\Billing\Entity\InvoiceItem', 'ii', 'WITH', 'ii.id = ci.invoiceItemId')
->where('IDENTITY(ii.invoice) = :invoiceId')
->setParameter('invoiceId', $invoiceId)
->getQuery()
->getArrayResult();
return array_column($rows, 'kind');
}
public function save(Claim $entity, bool $flush = true): void
{
$this->getEntityManager()->persist($entity);
@@ -29,6 +29,28 @@ class InvoiceRepository extends ServiceEntityRepository
return $this->findOneBy(['patientSessionId' => $patientSessionId]);
}
/**
* صورتحساب‌های نهایی‌شده‌ای که بیمه‌ای روی خودشان دارند ولی هیچ مطالبه‌ای برایشان
* ثبت نشده — بازمانده‌های دورانی که مطالبه فقط در یک مسیر ساخته می‌شد.
*
* @return Invoice[]
*/
public function findFinalizedInsuredWithoutClaims(): array
{
return $this->createQueryBuilder('i')
->where('i.status = :finalized')
->andWhere('i.baseInsuranceId IS NOT NULL OR i.supplementaryInsuranceId IS NOT NULL')
->andWhere('NOT EXISTS (
SELECT 1 FROM App\Billing\Entity\ClaimItem ci
JOIN App\Billing\Entity\InvoiceItem ii WITH ii.id = ci.invoiceItemId
WHERE IDENTITY(ii.invoice) = i.id
)')
->setParameter('finalized', Invoice::STATUS_FINALIZED)
->orderBy('i.id', 'ASC')
->getQuery()
->getResult();
}
/**
* A flat, newest-first page of a tenant's recorded (finalized/paid) invoices,
* one row per invoice with the patient's name and national code joined in.