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,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Insurance\Entity;
|
||||
|
||||
use App\Insurance\Repository\EntityInsurancePricingRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: EntityInsurancePricingRepository::class)]
|
||||
#[ORM\Table(name: 'entity_insurance_pricing')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_entity_insurance', columns: ['entity_type', 'entity_id', 'insurance_id'])]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id'], name: 'idx_entity_pricing_owner')]
|
||||
class EntityInsurancePricing
|
||||
{
|
||||
public const TYPE_DOCTOR = 'doctor';
|
||||
public const TYPE_CLINIC = 'clinic';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
private int $entityId;
|
||||
|
||||
#[ORM\Column(name: 'insurance_id', type: 'integer', nullable: true)]
|
||||
private ?int $insuranceId = null;
|
||||
|
||||
#[ORM\Column(name: 'patient_share_rials', type: 'integer')]
|
||||
private int $patientShareRials = 0;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(string $entityType, int $entityId, ?int $insuranceId, int $patientShareRials = 0)
|
||||
{
|
||||
$this->entityType = $entityType;
|
||||
$this->entityId = $entityId;
|
||||
$this->insuranceId = $insuranceId;
|
||||
$this->patientShareRials = $patientShareRials;
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getEntityType(): string { return $this->entityType; }
|
||||
public function getEntityId(): int { return $this->entityId; }
|
||||
public function getInsuranceId(): ?int { return $this->insuranceId; }
|
||||
public function getPatientShareRials(): int { return $this->patientShareRials; }
|
||||
|
||||
public function isFreeVisit(): bool { return $this->insuranceId === null; }
|
||||
|
||||
public function setPatientShareRials(int $v): self { $this->patientShareRials = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'insurance_id' => $this->insuranceId,
|
||||
'patient_share_rials' => $this->patientShareRials,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Insurance\Entity;
|
||||
|
||||
use App\Insurance\Repository\TenantInsuranceRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TenantInsuranceRepository::class)]
|
||||
#[ORM\Table(name: 'tenant_insurances')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_tenant_insurance_version', columns: ['entity_type', 'entity_id', 'insurance_id', 'version'])]
|
||||
#[ORM\Index(columns: ['entity_type', 'entity_id', 'is_active'], name: 'idx_tenant_insurance_active')]
|
||||
class TenantInsurance
|
||||
{
|
||||
public const TYPE_DOCTOR = 'doctor';
|
||||
public const TYPE_CLINIC = 'clinic';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'entity_type', type: 'string', length: 10)]
|
||||
private string $entityType;
|
||||
|
||||
#[ORM\Column(name: 'entity_id', type: 'integer')]
|
||||
private int $entityId;
|
||||
|
||||
#[ORM\Column(name: 'insurance_id', type: 'integer')]
|
||||
private int $insuranceId;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $version = 1;
|
||||
|
||||
#[ORM\Column(name: 'is_active', type: 'boolean')]
|
||||
private bool $isActive = true;
|
||||
|
||||
#[ORM\Column(name: 'coverage_percent', type: 'decimal', precision: 5, scale: 2)]
|
||||
private string $coveragePercent = '0.00';
|
||||
|
||||
#[ORM\Column(name: 'franchise_rials', type: 'integer')]
|
||||
private int $franchiseRials = 0;
|
||||
|
||||
#[ORM\Column(name: 'annual_ceiling_rials', type: 'integer', nullable: true)]
|
||||
private ?int $annualCeilingRials = null;
|
||||
|
||||
#[ORM\Column(name: 'effective_from', type: 'integer')]
|
||||
private int $effectiveFrom;
|
||||
|
||||
#[ORM\Column(name: 'effective_to', type: 'integer', nullable: true)]
|
||||
private ?int $effectiveTo = null;
|
||||
|
||||
#[ORM\Column(name: 'created_at', type: 'integer')]
|
||||
private int $createdAt;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(string $entityType, int $entityId, int $insuranceId, int $version = 1)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->entityType = $entityType;
|
||||
$this->entityId = $entityId;
|
||||
$this->insuranceId = $insuranceId;
|
||||
$this->version = $version;
|
||||
$this->effectiveFrom = time();
|
||||
$this->createdAt = time();
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getEntityType(): string { return $this->entityType; }
|
||||
public function getEntityId(): int { return $this->entityId; }
|
||||
public function getInsuranceId(): int { return $this->insuranceId; }
|
||||
public function getVersion(): int { return $this->version; }
|
||||
public function isActive(): bool { return $this->isActive; }
|
||||
public function getCoveragePercent(): float { return (float) $this->coveragePercent; }
|
||||
public function getFranchiseRials(): int { return $this->franchiseRials; }
|
||||
public function getAnnualCeilingRials(): ?int { return $this->annualCeilingRials; }
|
||||
public function getEffectiveFrom(): int { return $this->effectiveFrom; }
|
||||
public function getEffectiveTo(): ?int { return $this->effectiveTo; }
|
||||
|
||||
public function setActive(bool $v): self { $this->isActive = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setCoveragePercent(float $v): self { $this->coveragePercent = (string) $v; $this->updatedAt = time(); return $this; }
|
||||
public function setFranchiseRials(int $v): self { $this->franchiseRials = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setAnnualCeilingRials(?int $v): self { $this->annualCeilingRials = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setEffectiveTo(?int $v): self { $this->effectiveTo = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'entity_type' => $this->entityType,
|
||||
'entity_id' => $this->entityId,
|
||||
'insurance_id' => $this->insuranceId,
|
||||
'version' => $this->version,
|
||||
'is_active' => $this->isActive,
|
||||
'coverage_percent' => (float) $this->coveragePercent,
|
||||
'franchise_rials' => $this->franchiseRials,
|
||||
'annual_ceiling_rials' => $this->annualCeilingRials,
|
||||
'effective_from' => $this->effectiveFrom,
|
||||
'effective_to' => $this->effectiveTo,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Insurance\Entity;
|
||||
|
||||
use App\Insurance\Repository\TenantServiceCoverageRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Uid\Uuid;
|
||||
|
||||
#[ORM\Entity(repositoryClass: TenantServiceCoverageRepository::class)]
|
||||
#[ORM\Table(name: 'tenant_service_coverage')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_tenant_service_coverage', columns: ['tenant_insurance_id', 'service_item_id'])]
|
||||
#[ORM\Index(columns: ['service_item_id'], name: 'idx_tsc_service')]
|
||||
class TenantServiceCoverage
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 36, unique: true)]
|
||||
private string $uuid;
|
||||
|
||||
#[ORM\Column(name: 'tenant_insurance_id', type: 'integer')]
|
||||
private int $tenantInsuranceId;
|
||||
|
||||
#[ORM\Column(name: 'service_item_id', type: 'integer')]
|
||||
private int $serviceItemId;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $covered = true;
|
||||
|
||||
#[ORM\Column(name: 'coverage_percent', type: 'decimal', precision: 5, scale: 2, nullable: true)]
|
||||
private ?string $coveragePercent = null;
|
||||
|
||||
#[ORM\Column(name: 'franchise_rials', type: 'integer', nullable: true)]
|
||||
private ?int $franchiseRials = null;
|
||||
|
||||
#[ORM\Column(name: 'ceiling_rials', type: 'integer', nullable: true)]
|
||||
private ?int $ceilingRials = null;
|
||||
|
||||
#[ORM\Column(name: 'updated_at', type: 'integer')]
|
||||
private int $updatedAt;
|
||||
|
||||
public function __construct(int $tenantInsuranceId, int $serviceItemId)
|
||||
{
|
||||
$this->uuid = Uuid::v4()->toRfc4122();
|
||||
$this->tenantInsuranceId = $tenantInsuranceId;
|
||||
$this->serviceItemId = $serviceItemId;
|
||||
$this->updatedAt = time();
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
public function getUuid(): string { return $this->uuid; }
|
||||
public function getTenantInsuranceId(): int { return $this->tenantInsuranceId; }
|
||||
public function getServiceItemId(): int { return $this->serviceItemId; }
|
||||
public function isCovered(): bool { return $this->covered; }
|
||||
public function getCoveragePercent(): ?float { return $this->coveragePercent !== null ? (float) $this->coveragePercent : null; }
|
||||
public function getFranchiseRials(): ?int { return $this->franchiseRials; }
|
||||
public function getCeilingRials(): ?int { return $this->ceilingRials; }
|
||||
|
||||
public function setCovered(bool $v): self { $this->covered = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setCoveragePercent(?float $v): self { $this->coveragePercent = $v !== null ? (string) $v : null; $this->updatedAt = time(); return $this; }
|
||||
public function setFranchiseRials(?int $v): self { $this->franchiseRials = $v; $this->updatedAt = time(); return $this; }
|
||||
public function setCeilingRials(?int $v): self { $this->ceilingRials = $v; $this->updatedAt = time(); return $this; }
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $this->uuid,
|
||||
'tenant_insurance_id' => $this->tenantInsuranceId,
|
||||
'service_item_id' => $this->serviceItemId,
|
||||
'covered' => $this->covered,
|
||||
'coverage_percent' => $this->getCoveragePercent(),
|
||||
'franchise_rials' => $this->franchiseRials,
|
||||
'ceiling_rials' => $this->ceilingRials,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user