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:
hamed
2026-06-23 15:05:24 +03:30
co-authored by Claude Opus 4.8
parent 5b1dfe9b40
commit 89191eee57
54 changed files with 4233 additions and 10 deletions
@@ -0,0 +1,74 @@
<?php
namespace App\Insurance\Repository;
use App\Insurance\Entity\TenantInsurance;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class TenantInsuranceRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TenantInsurance::class);
}
/** @return TenantInsurance[] */
public function findActiveByTenant(string $entityType, int $entityId): array
{
return $this->createQueryBuilder('t')
->where('t.entityType = :type')
->andWhere('t.entityId = :id')
->andWhere('t.isActive = true')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->orderBy('t.insuranceId', 'ASC')
->getQuery()
->getResult();
}
public function findByUuid(string $uuid): ?TenantInsurance
{
return $this->findOneBy(['uuid' => $uuid]);
}
public function findActiveContract(string $entityType, int $entityId, int $insuranceId): ?TenantInsurance
{
return $this->createQueryBuilder('t')
->where('t.entityType = :type')
->andWhere('t.entityId = :id')
->andWhere('t.insuranceId = :ins')
->andWhere('t.isActive = true')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->setParameter('ins', $insuranceId)
->orderBy('t.version', 'DESC')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
}
public function latestVersion(string $entityType, int $entityId, int $insuranceId): int
{
$max = $this->createQueryBuilder('t')
->select('MAX(t.version)')
->where('t.entityType = :type')
->andWhere('t.entityId = :id')
->andWhere('t.insuranceId = :ins')
->setParameter('type', $entityType)
->setParameter('id', $entityId)
->setParameter('ins', $insuranceId)
->getQuery()
->getSingleScalarResult();
return (int) ($max ?? 0);
}
public function save(TenantInsurance $entity, bool $flush = true): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}