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
+32
View File
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622163152 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE entity_insurance_pricing (id INT AUTO_INCREMENT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, insurance_id INT DEFAULT NULL, patient_share_rials INT NOT NULL, updated_at INT NOT NULL, INDEX idx_entity_pricing_owner (entity_type, entity_id), UNIQUE INDEX uniq_entity_insurance (entity_type, entity_id, insurance_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql("INSERT INTO entity_insurance_pricing (entity_type, entity_id, insurance_id, patient_share_rials, updated_at) SELECT 'doctor', doctor_id, insurance_id, COALESCE(price, 0), UNIX_TIMESTAMP() FROM doctor_insurances WHERE price IS NOT NULL");
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE entity_insurance_pricing');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622171538 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE tenant_insurances (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, insurance_id INT NOT NULL, version INT NOT NULL, is_active TINYINT NOT NULL, coverage_percent NUMERIC(5, 2) NOT NULL, franchise_rials INT NOT NULL, annual_ceiling_rials INT DEFAULT NULL, effective_from INT NOT NULL, effective_to INT DEFAULT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, UNIQUE INDEX UNIQ_9C419AB7D17F50A6 (uuid), INDEX idx_tenant_insurance_active (entity_type, entity_id, is_active), UNIQUE INDEX uniq_tenant_insurance_version (entity_type, entity_id, insurance_id, version), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE tenant_insurances');
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622173707 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE tenant_service_coverage (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, tenant_insurance_id INT NOT NULL, service_item_id INT NOT NULL, covered TINYINT NOT NULL, coverage_percent NUMERIC(5, 2) DEFAULT NULL, franchise_rials INT DEFAULT NULL, ceiling_rials INT DEFAULT NULL, updated_at INT NOT NULL, UNIQUE INDEX UNIQ_76431C8ED17F50A6 (uuid), INDEX idx_tsc_service (service_item_id), UNIQUE INDEX uniq_tenant_service_coverage (tenant_insurance_id, service_item_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE service_items ADD insurance_covered TINYINT NOT NULL, ADD insurance_price_rials INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE tenant_service_coverage');
$this->addSql('ALTER TABLE service_items DROP insurance_covered, DROP insurance_price_rials');
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622181251 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE service_tariffs (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, service_item_id INT NOT NULL, year SMALLINT NOT NULL, price_rials INT NOT NULL, is_active TINYINT NOT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, UNIQUE INDEX UNIQ_FAAAF536D17F50A6 (uuid), INDEX idx_tariff_service_active (service_item_id, is_active), UNIQUE INDEX uniq_service_tariff_year (service_item_id, year), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE service_tariffs');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622184409 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE invoice_items (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, service_item_id INT DEFAULT NULL, title VARCHAR(200) NOT NULL, tariff_rials INT NOT NULL, quantity INT NOT NULL, total_rials INT NOT NULL, base_insurance_rials INT NOT NULL, supplementary_rials INT NOT NULL, patient_rials INT NOT NULL, invoice_id INT NOT NULL, UNIQUE INDEX UNIQ_DCC4B9F8D17F50A6 (uuid), INDEX IDX_DCC4B9F82989F1FD (invoice_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE invoices (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, patient_session_id INT DEFAULT NULL, patient_record_id INT DEFAULT NULL, base_insurance_id INT DEFAULT NULL, supplementary_insurance_id INT DEFAULT NULL, total_rials INT NOT NULL, base_insurance_rials INT NOT NULL, supplementary_rials INT NOT NULL, patient_rials INT NOT NULL, status VARCHAR(15) NOT NULL, issued_at INT NOT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, UNIQUE INDEX UNIQ_6A2F2F95D17F50A6 (uuid), INDEX idx_invoice_tenant (entity_type, entity_id), INDEX idx_invoice_session (patient_session_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE invoice_items ADD CONSTRAINT FK_DCC4B9F82989F1FD FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE invoice_items DROP FOREIGN KEY FK_DCC4B9F82989F1FD');
$this->addSql('DROP TABLE invoice_items');
$this->addSql('DROP TABLE invoices');
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260622185948 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE claim_items (id INT AUTO_INCREMENT NOT NULL, invoice_item_id INT NOT NULL, claimed_rials INT NOT NULL, approved_rials INT DEFAULT NULL, claim_id INT NOT NULL, INDEX IDX_DD53020B7096A49F (claim_id), INDEX idx_claim_item_invoice_item (invoice_item_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE claims (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, insurance_id INT NOT NULL, insurance_kind VARCHAR(15) NOT NULL, total_claimed_rials INT NOT NULL, total_approved_rials INT DEFAULT NULL, total_paid_rials INT DEFAULT NULL, status VARCHAR(15) NOT NULL, reject_reason LONGTEXT DEFAULT NULL, submitted_at INT DEFAULT NULL, settled_at INT DEFAULT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, UNIQUE INDEX UNIQ_BEA313BED17F50A6 (uuid), INDEX idx_claim_tenant (entity_type, entity_id), INDEX idx_claim_insurance_status (insurance_id, status), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE claim_items ADD CONSTRAINT FK_DD53020B7096A49F FOREIGN KEY (claim_id) REFERENCES claims (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE claim_items DROP FOREIGN KEY FK_DD53020B7096A49F');
$this->addSql('DROP TABLE claim_items');
$this->addSql('DROP TABLE claims');
}
}