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,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Billing\Repository;
|
||||
|
||||
use App\Billing\Entity\ClaimItem;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class ClaimItemRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ClaimItem::class);
|
||||
}
|
||||
|
||||
public function existsForInvoiceItem(int $invoiceItemId, int $insuranceKindClaimInsuranceId): bool
|
||||
{
|
||||
return $this->createQueryBuilder('ci')
|
||||
->select('COUNT(ci.id)')
|
||||
->join('ci.claim', 'c')
|
||||
->where('ci.invoiceItemId = :iid')
|
||||
->andWhere('c.insuranceId = :ins')
|
||||
->setParameter('iid', $invoiceItemId)
|
||||
->setParameter('ins', $insuranceKindClaimInsuranceId)
|
||||
->getQuery()
|
||||
->getSingleScalarResult() > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Billing\Repository;
|
||||
|
||||
use App\Billing\Entity\Claim;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class ClaimRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Claim::class);
|
||||
}
|
||||
|
||||
public function findByUuid(string $uuid): ?Claim
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
/** @return Claim[] */
|
||||
public function findByTenant(string $entityType, int $entityId, ?string $status = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->where('c.entityType = :type')
|
||||
->andWhere('c.entityId = :id')
|
||||
->setParameter('type', $entityType)
|
||||
->setParameter('id', $entityId)
|
||||
->orderBy('c.id', 'DESC');
|
||||
|
||||
if ($status !== null) {
|
||||
$qb->andWhere('c.status = :status')->setParameter('status', $status);
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* گزارش بدهی بیمه: جمع claimed/approved/paid بر اساس بیمه برای tenant.
|
||||
* @return array<int, array{insurance_id:int, claimed:int, approved:int, paid:int, debt:int}>
|
||||
*/
|
||||
public function debtReport(string $entityType, int $entityId): array
|
||||
{
|
||||
$rows = $this->createQueryBuilder('c')
|
||||
->select('c.insuranceId AS insurance_id')
|
||||
->addSelect('SUM(c.totalClaimedRials) AS claimed')
|
||||
->addSelect('SUM(COALESCE(c.totalApprovedRials, 0)) AS approved')
|
||||
->addSelect('SUM(COALESCE(c.totalPaidRials, 0)) AS paid')
|
||||
->where('c.entityType = :type')
|
||||
->andWhere('c.entityId = :id')
|
||||
->setParameter('type', $entityType)
|
||||
->setParameter('id', $entityId)
|
||||
->groupBy('c.insuranceId')
|
||||
->getQuery()
|
||||
->getArrayResult();
|
||||
|
||||
return array_map(function (array $r) {
|
||||
$claimed = (int) $r['claimed'];
|
||||
$paid = (int) $r['paid'];
|
||||
return [
|
||||
'insurance_id' => (int) $r['insurance_id'],
|
||||
'claimed' => $claimed,
|
||||
'approved' => (int) $r['approved'],
|
||||
'paid' => $paid,
|
||||
'debt' => max(0, $claimed - $paid),
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
public function save(Claim $entity, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Billing\Repository;
|
||||
|
||||
use App\Billing\Entity\InvoiceItem;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class InvoiceItemRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, InvoiceItem::class);
|
||||
}
|
||||
|
||||
public function save(InvoiceItem $entity, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Billing\Repository;
|
||||
|
||||
use App\Billing\Entity\Invoice;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class InvoiceRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Invoice::class);
|
||||
}
|
||||
|
||||
public function findByUuid(string $uuid): ?Invoice
|
||||
{
|
||||
return $this->findOneBy(['uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function findBySession(int $patientSessionId): ?Invoice
|
||||
{
|
||||
return $this->findOneBy(['patientSessionId' => $patientSessionId]);
|
||||
}
|
||||
|
||||
public function save(Invoice $entity, bool $flush = true): void
|
||||
{
|
||||
$this->getEntityManager()->persist($entity);
|
||||
if ($flush) {
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user