[CL-010] feat: Implement insurance module for medical insurance management

Implements complete insurance module with:
- Insurance company catalog (private, government, HMO, PPO)
- Insurance plans with coverage details, deductibles, copays
- Patient insurance policies with verification workflow
- Insurance claims with full lifecycle (draft->submitted->approved/denied/paid)
- Preauthorization requests with approval workflow
- Coverage verification endpoint
- EOB (Explanation of Benefits) tracking
- Aging reports for claims management

Entities: InsuranceCompany, InsurancePlan, PatientInsurance, InsuranceClaim, Preauthorization
Services: Full CRUD + workflow operations for each entity
Controller: 50+ REST endpoints for all insurance operations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-30 19:59:47 -06:00
parent e4d915889a
commit 1b38818354
17 changed files with 4560 additions and 0 deletions

View File

@ -0,0 +1 @@
export { InsuranceController } from './insurance.controller';

View File

@ -0,0 +1,968 @@
import { Router, Request, Response, NextFunction } from 'express';
import { DataSource } from 'typeorm';
import {
InsuranceCompanyService,
InsurancePlanService,
PatientInsuranceService,
ClaimService,
PreauthorizationService,
} from '../services';
import {
CreateInsuranceCompanyDto,
UpdateInsuranceCompanyDto,
InsuranceCompanyQueryDto,
CreateInsurancePlanDto,
UpdateInsurancePlanDto,
InsurancePlanQueryDto,
CreatePatientInsuranceDto,
UpdatePatientInsuranceDto,
VerifyInsuranceDto,
PatientInsuranceQueryDto,
VerifyCoverageDto,
CreateInsuranceClaimDto,
UpdateInsuranceClaimDto,
SubmitClaimDto,
ProcessClaimDto,
RecordEOBDto,
VoidClaimDto,
InsuranceClaimQueryDto,
CreatePreauthorizationDto,
UpdatePreauthorizationDto,
SubmitPreauthorizationDto,
ProcessPreauthorizationDto,
CancelPreauthorizationDto,
PreauthorizationQueryDto,
} from '../dto';
export class InsuranceController {
public router: Router;
private companyService: InsuranceCompanyService;
private planService: InsurancePlanService;
private patientInsuranceService: PatientInsuranceService;
private claimService: ClaimService;
private preauthorizationService: PreauthorizationService;
constructor(dataSource: DataSource, basePath: string = '/api') {
this.router = Router();
this.companyService = new InsuranceCompanyService(dataSource);
this.planService = new InsurancePlanService(dataSource);
this.patientInsuranceService = new PatientInsuranceService(dataSource);
this.claimService = new ClaimService(dataSource);
this.preauthorizationService = new PreauthorizationService(dataSource);
this.setupRoutes(basePath);
}
private setupRoutes(basePath: string): void {
const insurancePath = `${basePath}/insurance`;
// Insurance Companies Routes
this.router.get(`${insurancePath}/companies`, this.findAllCompanies.bind(this));
this.router.get(`${insurancePath}/companies/active`, this.findActiveCompanies.bind(this));
this.router.get(`${insurancePath}/companies/by-type`, this.getCompaniesByType.bind(this));
this.router.get(`${insurancePath}/companies/:id`, this.findCompanyById.bind(this));
this.router.post(`${insurancePath}/companies`, this.createCompany.bind(this));
this.router.patch(`${insurancePath}/companies/:id`, this.updateCompany.bind(this));
this.router.delete(`${insurancePath}/companies/:id`, this.deleteCompany.bind(this));
// Insurance Plans Routes
this.router.get(`${insurancePath}/plans`, this.findAllPlans.bind(this));
this.router.get(`${insurancePath}/plans/active`, this.findActivePlans.bind(this));
this.router.get(`${insurancePath}/plans/by-coverage-type`, this.getPlansByCoverageType.bind(this));
this.router.get(`${insurancePath}/plans/company/:companyId`, this.findPlansByCompany.bind(this));
this.router.get(`${insurancePath}/plans/:id`, this.findPlanById.bind(this));
this.router.post(`${insurancePath}/plans`, this.createPlan.bind(this));
this.router.patch(`${insurancePath}/plans/:id`, this.updatePlan.bind(this));
this.router.delete(`${insurancePath}/plans/:id`, this.deletePlan.bind(this));
// Patient Insurance Routes
this.router.get(`${insurancePath}/patient-insurance`, this.findAllPatientInsurance.bind(this));
this.router.get(`${insurancePath}/patient-insurance/unverified-count`, this.getUnverifiedCount.bind(this));
this.router.get(`${insurancePath}/patient-insurance/expiring`, this.getExpiringPolicies.bind(this));
this.router.get(`${insurancePath}/patient-insurance/patient/:patientId`, this.findPatientInsuranceByPatient.bind(this));
this.router.get(`${insurancePath}/patient-insurance/patient/:patientId/active`, this.findActivePatientInsurance.bind(this));
this.router.get(`${insurancePath}/patient-insurance/patient/:patientId/primary`, this.findPrimaryInsurance.bind(this));
this.router.get(`${insurancePath}/patient-insurance/:id`, this.findPatientInsuranceById.bind(this));
this.router.post(`${insurancePath}/patient-insurance`, this.createPatientInsurance.bind(this));
this.router.patch(`${insurancePath}/patient-insurance/:id`, this.updatePatientInsurance.bind(this));
this.router.post(`${insurancePath}/patient-insurance/:id/verify`, this.verifyPatientInsurance.bind(this));
this.router.post(`${insurancePath}/patient-insurance/:id/terminate`, this.terminatePatientInsurance.bind(this));
this.router.post(`${insurancePath}/patient-insurance/verify-coverage`, this.verifyCoverage.bind(this));
// Claims Routes
this.router.get(`${insurancePath}/claims`, this.findAllClaims.bind(this));
this.router.get(`${insurancePath}/claims/stats`, this.getClaimStats.bind(this));
this.router.get(`${insurancePath}/claims/aging`, this.getClaimAgingReport.bind(this));
this.router.get(`${insurancePath}/claims/patient/:patientId`, this.findClaimsByPatient.bind(this));
this.router.get(`${insurancePath}/claims/:id`, this.findClaimById.bind(this));
this.router.post(`${insurancePath}/claims`, this.createClaim.bind(this));
this.router.patch(`${insurancePath}/claims/:id`, this.updateClaim.bind(this));
this.router.post(`${insurancePath}/claims/:id/submit`, this.submitClaim.bind(this));
this.router.post(`${insurancePath}/claims/:id/process`, this.processClaim.bind(this));
this.router.post(`${insurancePath}/claims/:id/eob`, this.recordEOB.bind(this));
this.router.post(`${insurancePath}/claims/:id/appeal`, this.appealClaim.bind(this));
this.router.post(`${insurancePath}/claims/:id/void`, this.voidClaim.bind(this));
// Preauthorizations Routes
this.router.get(`${insurancePath}/preauthorizations`, this.findAllPreauthorizations.bind(this));
this.router.get(`${insurancePath}/preauthorizations/stats`, this.getPreauthorizationStats.bind(this));
this.router.get(`${insurancePath}/preauthorizations/urgent`, this.getUrgentPreauthorizations.bind(this));
this.router.get(`${insurancePath}/preauthorizations/expiring`, this.getExpiringPreauthorizations.bind(this));
this.router.get(`${insurancePath}/preauthorizations/patient/:patientId`, this.findPreauthorizationsByPatient.bind(this));
this.router.get(`${insurancePath}/preauthorizations/patient/:patientId/active`, this.findActivePreauthorizations.bind(this));
this.router.get(`${insurancePath}/preauthorizations/:id`, this.findPreauthorizationById.bind(this));
this.router.post(`${insurancePath}/preauthorizations`, this.createPreauthorization.bind(this));
this.router.patch(`${insurancePath}/preauthorizations/:id`, this.updatePreauthorization.bind(this));
this.router.post(`${insurancePath}/preauthorizations/:id/submit`, this.submitPreauthorization.bind(this));
this.router.post(`${insurancePath}/preauthorizations/:id/pending`, this.markPreauthorizationPending.bind(this));
this.router.post(`${insurancePath}/preauthorizations/:id/process`, this.processPreauthorization.bind(this));
this.router.post(`${insurancePath}/preauthorizations/:id/cancel`, this.cancelPreauthorization.bind(this));
}
private getTenantId(req: Request): string {
const tenantId = req.headers['x-tenant-id'] as string;
if (!tenantId) {
throw new Error('x-tenant-id header is required');
}
return tenantId;
}
// Insurance Companies Handlers
private async findAllCompanies(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const query: InsuranceCompanyQueryDto = {
search: req.query.search as string,
type: req.query.type as any,
status: req.query.status as any,
page: req.query.page ? parseInt(req.query.page as string) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
};
const result = await this.companyService.findAll(tenantId, query);
res.json({
data: result.data,
meta: {
total: result.total,
page: query.page,
limit: query.limit,
totalPages: Math.ceil(result.total / (query.limit || 50)),
},
});
} catch (error) {
next(error);
}
}
private async findActiveCompanies(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const companies = await this.companyService.findActive(tenantId);
res.json({ data: companies });
} catch (error) {
next(error);
}
}
private async getCompaniesByType(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const types = await this.companyService.getByType(tenantId);
res.json({ data: types });
} catch (error) {
next(error);
}
}
private async findCompanyById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const company = await this.companyService.findById(tenantId, id);
if (!company) {
res.status(404).json({ error: 'Insurance company not found', code: 'INSURANCE_COMPANY_NOT_FOUND' });
return;
}
res.json({ data: company });
} catch (error) {
next(error);
}
}
private async createCompany(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: CreateInsuranceCompanyDto = req.body;
const company = await this.companyService.create(tenantId, dto);
res.status(201).json({ data: company });
} catch (error) {
next(error);
}
}
private async updateCompany(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: UpdateInsuranceCompanyDto = req.body;
const company = await this.companyService.update(tenantId, id, dto);
if (!company) {
res.status(404).json({ error: 'Insurance company not found', code: 'INSURANCE_COMPANY_NOT_FOUND' });
return;
}
res.json({ data: company });
} catch (error) {
next(error);
}
}
private async deleteCompany(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const deleted = await this.companyService.softDelete(tenantId, id);
if (!deleted) {
res.status(404).json({ error: 'Insurance company not found', code: 'INSURANCE_COMPANY_NOT_FOUND' });
return;
}
res.status(204).send();
} catch (error) {
next(error);
}
}
// Insurance Plans Handlers
private async findAllPlans(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const query: InsurancePlanQueryDto = {
search: req.query.search as string,
insuranceCompanyId: req.query.insuranceCompanyId as string,
coverageType: req.query.coverageType as any,
status: req.query.status as any,
page: req.query.page ? parseInt(req.query.page as string) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
};
const result = await this.planService.findAll(tenantId, query);
res.json({
data: result.data,
meta: {
total: result.total,
page: query.page,
limit: query.limit,
totalPages: Math.ceil(result.total / (query.limit || 50)),
},
});
} catch (error) {
next(error);
}
}
private async findActivePlans(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const plans = await this.planService.findActive(tenantId);
res.json({ data: plans });
} catch (error) {
next(error);
}
}
private async getPlansByCoverageType(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const types = await this.planService.getByCoverageType(tenantId);
res.json({ data: types });
} catch (error) {
next(error);
}
}
private async findPlansByCompany(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { companyId } = req.params;
const plans = await this.planService.findByCompany(tenantId, companyId);
res.json({ data: plans });
} catch (error) {
next(error);
}
}
private async findPlanById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const plan = await this.planService.findById(tenantId, id);
if (!plan) {
res.status(404).json({ error: 'Insurance plan not found', code: 'INSURANCE_PLAN_NOT_FOUND' });
return;
}
res.json({ data: plan });
} catch (error) {
next(error);
}
}
private async createPlan(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: CreateInsurancePlanDto = req.body;
const plan = await this.planService.create(tenantId, dto);
res.status(201).json({ data: plan });
} catch (error) {
next(error);
}
}
private async updatePlan(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: UpdateInsurancePlanDto = req.body;
const plan = await this.planService.update(tenantId, id, dto);
if (!plan) {
res.status(404).json({ error: 'Insurance plan not found', code: 'INSURANCE_PLAN_NOT_FOUND' });
return;
}
res.json({ data: plan });
} catch (error) {
next(error);
}
}
private async deletePlan(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const deleted = await this.planService.softDelete(tenantId, id);
if (!deleted) {
res.status(404).json({ error: 'Insurance plan not found', code: 'INSURANCE_PLAN_NOT_FOUND' });
return;
}
res.status(204).send();
} catch (error) {
next(error);
}
}
// Patient Insurance Handlers
private async findAllPatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const query: PatientInsuranceQueryDto = {
patientId: req.query.patientId as string,
insurancePlanId: req.query.insurancePlanId as string,
priority: req.query.priority as any,
status: req.query.status as any,
isVerified: req.query.isVerified === 'true' ? true : req.query.isVerified === 'false' ? false : undefined,
page: req.query.page ? parseInt(req.query.page as string) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string) : 50,
};
const result = await this.patientInsuranceService.findAll(tenantId, query);
res.json({
data: result.data,
meta: {
total: result.total,
page: query.page,
limit: query.limit,
totalPages: Math.ceil(result.total / (query.limit || 50)),
},
});
} catch (error) {
next(error);
}
}
private async getUnverifiedCount(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const count = await this.patientInsuranceService.getUnverifiedCount(tenantId);
res.json({ data: { count } });
} catch (error) {
next(error);
}
}
private async getExpiringPolicies(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const daysAhead = req.query.daysAhead ? parseInt(req.query.daysAhead as string) : 30;
const policies = await this.patientInsuranceService.getExpiringPolicies(tenantId, daysAhead);
res.json({ data: policies });
} catch (error) {
next(error);
}
}
private async findPatientInsuranceByPatient(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const policies = await this.patientInsuranceService.findByPatient(tenantId, patientId);
res.json({ data: policies });
} catch (error) {
next(error);
}
}
private async findActivePatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const policies = await this.patientInsuranceService.findActiveByPatient(tenantId, patientId);
res.json({ data: policies });
} catch (error) {
next(error);
}
}
private async findPrimaryInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const policy = await this.patientInsuranceService.findPrimaryByPatient(tenantId, patientId);
res.json({ data: policy });
} catch (error) {
next(error);
}
}
private async findPatientInsuranceById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const policy = await this.patientInsuranceService.findById(tenantId, id);
if (!policy) {
res.status(404).json({ error: 'Patient insurance not found', code: 'PATIENT_INSURANCE_NOT_FOUND' });
return;
}
res.json({ data: policy });
} catch (error) {
next(error);
}
}
private async createPatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: CreatePatientInsuranceDto = req.body;
const policy = await this.patientInsuranceService.create(tenantId, dto);
res.status(201).json({ data: policy });
} catch (error) {
next(error);
}
}
private async updatePatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: UpdatePatientInsuranceDto = req.body;
const policy = await this.patientInsuranceService.update(tenantId, id, dto);
if (!policy) {
res.status(404).json({ error: 'Patient insurance not found', code: 'PATIENT_INSURANCE_NOT_FOUND' });
return;
}
res.json({ data: policy });
} catch (error) {
next(error);
}
}
private async verifyPatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: VerifyInsuranceDto = req.body;
const policy = await this.patientInsuranceService.verify(tenantId, id, dto);
if (!policy) {
res.status(404).json({ error: 'Patient insurance not found', code: 'PATIENT_INSURANCE_NOT_FOUND' });
return;
}
res.json({ data: policy });
} catch (error) {
next(error);
}
}
private async terminatePatientInsurance(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const terminationDate = req.body.terminationDate ? new Date(req.body.terminationDate) : undefined;
const policy = await this.patientInsuranceService.terminate(tenantId, id, terminationDate);
if (!policy) {
res.status(404).json({ error: 'Patient insurance not found', code: 'PATIENT_INSURANCE_NOT_FOUND' });
return;
}
res.json({ data: policy });
} catch (error) {
next(error);
}
}
private async verifyCoverage(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: VerifyCoverageDto = req.body;
const result = await this.patientInsuranceService.verifyCoverage(tenantId, dto);
res.json({ data: result });
} catch (error) {
next(error);
}
}
// Claims Handlers
private async findAllClaims(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const query: InsuranceClaimQueryDto = {
patientId: req.query.patientId as string,
patientInsuranceId: req.query.patientInsuranceId as string,
status: req.query.status as any,
claimType: req.query.claimType as any,
dateFrom: req.query.dateFrom as string,
dateTo: req.query.dateTo as string,
page: req.query.page ? parseInt(req.query.page as string) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
};
const result = await this.claimService.findAll(tenantId, query);
res.json({
data: result.data,
meta: {
total: result.total,
page: query.page,
limit: query.limit,
totalPages: Math.ceil(result.total / (query.limit || 20)),
},
});
} catch (error) {
next(error);
}
}
private async getClaimStats(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const [statusBreakdown, pendingCount] = await Promise.all([
this.claimService.getClaimsByStatus(tenantId),
this.claimService.getPendingClaimsCount(tenantId),
]);
res.json({
data: {
statusBreakdown,
pendingCount,
},
});
} catch (error) {
next(error);
}
}
private async getClaimAgingReport(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const aging = await this.claimService.getAgingReport(tenantId);
res.json({ data: aging });
} catch (error) {
next(error);
}
}
private async findClaimsByPatient(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
const claims = await this.claimService.findByPatient(tenantId, patientId, limit);
res.json({ data: claims });
} catch (error) {
next(error);
}
}
private async findClaimById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const claim = await this.claimService.findById(tenantId, id);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async createClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: CreateInsuranceClaimDto = req.body;
const claim = await this.claimService.create(tenantId, dto);
res.status(201).json({ data: claim });
} catch (error) {
next(error);
}
}
private async updateClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: UpdateInsuranceClaimDto = req.body;
const claim = await this.claimService.update(tenantId, id, dto);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async submitClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: SubmitClaimDto = req.body;
const claim = await this.claimService.submit(tenantId, id, dto);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async processClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: ProcessClaimDto = req.body;
const claim = await this.claimService.process(tenantId, id, dto);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async recordEOB(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: RecordEOBDto = req.body;
const claim = await this.claimService.recordEOB(tenantId, id, dto);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async appealClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const { notes } = req.body;
const claim = await this.claimService.appeal(tenantId, id, notes);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
private async voidClaim(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: VoidClaimDto = req.body;
const claim = await this.claimService.void(tenantId, id, dto);
if (!claim) {
res.status(404).json({ error: 'Insurance claim not found', code: 'INSURANCE_CLAIM_NOT_FOUND' });
return;
}
res.json({ data: claim });
} catch (error) {
next(error);
}
}
// Preauthorizations Handlers
private async findAllPreauthorizations(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const query: PreauthorizationQueryDto = {
patientId: req.query.patientId as string,
patientInsuranceId: req.query.patientInsuranceId as string,
status: req.query.status as any,
authType: req.query.authType as any,
urgency: req.query.urgency as any,
dateFrom: req.query.dateFrom as string,
dateTo: req.query.dateTo as string,
page: req.query.page ? parseInt(req.query.page as string) : 1,
limit: req.query.limit ? parseInt(req.query.limit as string) : 20,
};
const result = await this.preauthorizationService.findAll(tenantId, query);
res.json({
data: result.data,
meta: {
total: result.total,
page: query.page,
limit: query.limit,
totalPages: Math.ceil(result.total / (query.limit || 20)),
},
});
} catch (error) {
next(error);
}
}
private async getPreauthorizationStats(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const [statusBreakdown, pendingCount] = await Promise.all([
this.preauthorizationService.getPreauthorizationsByStatus(tenantId),
this.preauthorizationService.getPendingCount(tenantId),
]);
res.json({
data: {
statusBreakdown,
pendingCount,
},
});
} catch (error) {
next(error);
}
}
private async getUrgentPreauthorizations(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const preauths = await this.preauthorizationService.getUrgentPreauthorizations(tenantId);
res.json({ data: preauths });
} catch (error) {
next(error);
}
}
private async getExpiringPreauthorizations(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const daysAhead = req.query.daysAhead ? parseInt(req.query.daysAhead as string) : 14;
const preauths = await this.preauthorizationService.getExpiringPreauthorizations(tenantId, daysAhead);
res.json({ data: preauths });
} catch (error) {
next(error);
}
}
private async findPreauthorizationsByPatient(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit as string) : 10;
const preauths = await this.preauthorizationService.findByPatient(tenantId, patientId, limit);
res.json({ data: preauths });
} catch (error) {
next(error);
}
}
private async findActivePreauthorizations(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { patientId } = req.params;
const preauths = await this.preauthorizationService.findActive(tenantId, patientId);
res.json({ data: preauths });
} catch (error) {
next(error);
}
}
private async findPreauthorizationById(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const preauth = await this.preauthorizationService.findById(tenantId, id);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
private async createPreauthorization(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const dto: CreatePreauthorizationDto = req.body;
const preauth = await this.preauthorizationService.create(tenantId, dto);
res.status(201).json({ data: preauth });
} catch (error) {
next(error);
}
}
private async updatePreauthorization(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: UpdatePreauthorizationDto = req.body;
const preauth = await this.preauthorizationService.update(tenantId, id, dto);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
private async submitPreauthorization(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: SubmitPreauthorizationDto = req.body;
const preauth = await this.preauthorizationService.submit(tenantId, id, dto);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
private async markPreauthorizationPending(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const { payerReferenceNumber } = req.body;
const preauth = await this.preauthorizationService.markPending(tenantId, id, payerReferenceNumber);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
private async processPreauthorization(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: ProcessPreauthorizationDto = req.body;
const preauth = await this.preauthorizationService.process(tenantId, id, dto);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
private async cancelPreauthorization(req: Request, res: Response, next: NextFunction): Promise<void> {
try {
const tenantId = this.getTenantId(req);
const { id } = req.params;
const dto: CancelPreauthorizationDto = req.body;
const preauth = await this.preauthorizationService.cancel(tenantId, id, dto);
if (!preauth) {
res.status(404).json({ error: 'Preauthorization not found', code: 'PREAUTHORIZATION_NOT_FOUND' });
return;
}
res.json({ data: preauth });
} catch (error) {
next(error);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,48 @@
export {
InsuranceCompany,
InsuranceCompanyStatus,
InsuranceCompanyType,
ContactInfo,
AddressInfo,
} from './insurance-company.entity';
export {
InsurancePlan,
InsurancePlanStatus,
CoverageType,
CoverageDetails,
DeductibleInfo,
CopayInfo,
OutOfPocketMax,
} from './insurance-plan.entity';
export {
PatientInsurance,
PatientInsuranceStatus,
InsurancePriority,
RelationshipToSubscriber,
SubscriberInfo,
DeductibleUsage,
BenefitUsage,
} from './patient-insurance.entity';
export {
InsuranceClaim,
ClaimStatus,
ClaimType,
ClaimLineItem,
EOBInfo,
DenialInfo,
ClaimAdjustment,
} from './insurance-claim.entity';
export {
Preauthorization,
PreauthorizationStatus,
PreauthorizationType,
UrgencyLevel,
RequestedService,
ApprovalDetails,
DenialDetails,
StatusHistory,
} from './preauthorization.entity';

View File

@ -0,0 +1,184 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { PatientInsurance } from './patient-insurance.entity';
export type ClaimStatus = 'draft' | 'submitted' | 'pending' | 'in_review' | 'approved' | 'denied' | 'partial' | 'appealed' | 'paid' | 'voided';
export type ClaimType = 'professional' | 'institutional' | 'dental' | 'pharmacy';
export interface ClaimLineItem {
id: string;
serviceDate: string;
procedureCode: string;
procedureDescription?: string;
diagnosisCodes?: string[];
units: number;
chargeAmount: number;
allowedAmount?: number;
paidAmount?: number;
deniedAmount?: number;
adjustmentAmount?: number;
adjustmentReasonCode?: string;
modifiers?: string[];
placeOfService?: string;
renderingProviderId?: string;
}
export interface EOBInfo {
receivedDate?: string;
processedDate?: string;
checkNumber?: string;
checkDate?: string;
eobDocumentUrl?: string;
paymentMethod?: 'check' | 'eft' | 'virtual_card';
remarkCodes?: string[];
adjustmentReasonCodes?: string[];
}
export interface DenialInfo {
reasonCode?: string;
reasonDescription?: string;
appealDeadline?: string;
additionalInfoRequired?: string[];
}
export interface ClaimAdjustment {
date: string;
type: 'correction' | 'void' | 'resubmission';
reason: string;
adjustedBy: string;
previousStatus: ClaimStatus;
newStatus: ClaimStatus;
}
@Entity({ name: 'insurance_claims', schema: 'clinica' })
export class InsuranceClaim {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'claim_number', type: 'varchar', length: 50 })
claimNumber: string;
@Index()
@Column({ name: 'patient_id', type: 'uuid' })
patientId: string;
@Index()
@Column({ name: 'patient_insurance_id', type: 'uuid' })
patientInsuranceId: string;
@ManyToOne(() => PatientInsurance)
@JoinColumn({ name: 'patient_insurance_id' })
patientInsurance: PatientInsurance;
@Column({ name: 'consultation_id', type: 'uuid', nullable: true })
consultationId?: string;
@Column({ name: 'preauthorization_id', type: 'uuid', nullable: true })
preauthorizationId?: string;
@Column({ name: 'claim_type', type: 'enum', enum: ['professional', 'institutional', 'dental', 'pharmacy'], default: 'professional' })
claimType: ClaimType;
@Column({ type: 'enum', enum: ['draft', 'submitted', 'pending', 'in_review', 'approved', 'denied', 'partial', 'appealed', 'paid', 'voided'], default: 'draft' })
status: ClaimStatus;
@Column({ name: 'service_date_from', type: 'date' })
serviceDateFrom: Date;
@Column({ name: 'service_date_to', type: 'date' })
serviceDateTo: Date;
@Column({ name: 'diagnosis_codes', type: 'jsonb', nullable: true })
diagnosisCodes?: string[];
@Column({ name: 'primary_diagnosis_code', type: 'varchar', length: 20, nullable: true })
primaryDiagnosisCode?: string;
@Column({ name: 'line_items', type: 'jsonb' })
lineItems: ClaimLineItem[];
@Column({ name: 'total_charge_amount', type: 'decimal', precision: 12, scale: 2 })
totalChargeAmount: number;
@Column({ name: 'total_allowed_amount', type: 'decimal', precision: 12, scale: 2, nullable: true })
totalAllowedAmount?: number;
@Column({ name: 'total_paid_amount', type: 'decimal', precision: 12, scale: 2, nullable: true })
totalPaidAmount?: number;
@Column({ name: 'total_denied_amount', type: 'decimal', precision: 12, scale: 2, nullable: true })
totalDeniedAmount?: number;
@Column({ name: 'total_adjustment_amount', type: 'decimal', precision: 12, scale: 2, nullable: true })
totalAdjustmentAmount?: number;
@Column({ name: 'patient_responsibility', type: 'decimal', precision: 12, scale: 2, nullable: true })
patientResponsibility?: number;
@Column({ name: 'deductible_applied', type: 'decimal', precision: 10, scale: 2, nullable: true })
deductibleApplied?: number;
@Column({ name: 'copay_applied', type: 'decimal', precision: 10, scale: 2, nullable: true })
copayApplied?: number;
@Column({ name: 'coinsurance_applied', type: 'decimal', precision: 10, scale: 2, nullable: true })
coinsuranceApplied?: number;
@Column({ name: 'billing_provider_id', type: 'uuid', nullable: true })
billingProviderId?: string;
@Column({ name: 'rendering_provider_id', type: 'uuid', nullable: true })
renderingProviderId?: string;
@Column({ name: 'place_of_service', type: 'varchar', length: 10, nullable: true })
placeOfService?: string;
@Column({ name: 'submitted_at', type: 'timestamptz', nullable: true })
submittedAt?: Date;
@Column({ name: 'submitted_by', type: 'uuid', nullable: true })
submittedBy?: string;
@Column({ name: 'payer_claim_number', type: 'varchar', length: 100, nullable: true })
payerClaimNumber?: string;
@Column({ name: 'eob_info', type: 'jsonb', nullable: true })
eobInfo?: EOBInfo;
@Column({ name: 'denial_info', type: 'jsonb', nullable: true })
denialInfo?: DenialInfo;
@Column({ name: 'adjustments', type: 'jsonb', nullable: true })
adjustments?: ClaimAdjustment[];
@Column({ name: 'filing_deadline', type: 'date', nullable: true })
filingDeadline?: Date;
@Column({ name: 'is_secondary_claim', type: 'boolean', default: false })
isSecondaryClaim: boolean;
@Column({ name: 'primary_claim_id', type: 'uuid', nullable: true })
primaryClaimId?: string;
@Column({ type: 'text', nullable: true })
notes?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,107 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
OneToMany,
} from 'typeorm';
import { InsurancePlan } from './insurance-plan.entity';
export type InsuranceCompanyStatus = 'active' | 'inactive' | 'suspended';
export type InsuranceCompanyType = 'private' | 'government' | 'employer' | 'hmo' | 'ppo' | 'other';
export interface ContactInfo {
phone?: string;
email?: string;
fax?: string;
website?: string;
claimsPhone?: string;
claimsEmail?: string;
preAuthPhone?: string;
preAuthEmail?: string;
}
export interface AddressInfo {
street?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
}
@Entity({ name: 'insurance_companies', schema: 'clinica' })
export class InsuranceCompany {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ type: 'varchar', length: 50 })
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ name: 'legal_name', type: 'varchar', length: 200, nullable: true })
legalName?: string;
@Column({ name: 'tax_id', type: 'varchar', length: 50, nullable: true })
taxId?: string;
@Column({ type: 'enum', enum: ['private', 'government', 'employer', 'hmo', 'ppo', 'other'], default: 'private' })
type: InsuranceCompanyType;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'contact_info', type: 'jsonb', nullable: true })
contactInfo?: ContactInfo;
@Column({ name: 'address', type: 'jsonb', nullable: true })
address?: AddressInfo;
@Column({ name: 'billing_address', type: 'jsonb', nullable: true })
billingAddress?: AddressInfo;
@Column({ name: 'payer_id', type: 'varchar', length: 50, nullable: true })
payerId?: string;
@Column({ name: 'naic_code', type: 'varchar', length: 20, nullable: true })
naicCode?: string;
@Column({ name: 'average_payment_days', type: 'int', default: 30 })
averagePaymentDays: number;
@Column({ name: 'requires_preauthorization', type: 'boolean', default: false })
requiresPreauthorization: boolean;
@Column({ name: 'electronic_claims_enabled', type: 'boolean', default: false })
electronicClaimsEnabled: boolean;
@Column({ name: 'logo_url', type: 'varchar', length: 500, nullable: true })
logoUrl?: string;
@Column({ type: 'enum', enum: ['active', 'inactive', 'suspended'], default: 'active' })
status: InsuranceCompanyStatus;
@Column({ type: 'text', nullable: true })
notes?: string;
@OneToMany(() => InsurancePlan, (plan) => plan.insuranceCompany)
plans?: InsurancePlan[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt?: Date;
}

View File

@ -0,0 +1,148 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
DeleteDateColumn,
Index,
ManyToOne,
JoinColumn,
OneToMany,
} from 'typeorm';
import { InsuranceCompany } from './insurance-company.entity';
import { PatientInsurance } from './patient-insurance.entity';
export type InsurancePlanStatus = 'active' | 'inactive' | 'discontinued';
export type CoverageType = 'individual' | 'family' | 'group' | 'medicare' | 'medicaid' | 'other';
export interface CoverageDetails {
consultations?: number;
hospitalizations?: number;
emergencies?: number;
surgeries?: number;
laboratory?: number;
imaging?: number;
pharmacy?: number;
dental?: number;
vision?: number;
mentalHealth?: number;
preventiveCare?: number;
}
export interface DeductibleInfo {
individual?: number;
family?: number;
inNetwork?: number;
outOfNetwork?: number;
}
export interface CopayInfo {
primaryCare?: number;
specialist?: number;
urgentCare?: number;
emergency?: number;
laboratory?: number;
imaging?: number;
genericDrugs?: number;
brandDrugs?: number;
specialtyDrugs?: number;
}
export interface OutOfPocketMax {
individual?: number;
family?: number;
inNetwork?: number;
outOfNetwork?: number;
}
@Entity({ name: 'insurance_plans', schema: 'clinica' })
export class InsurancePlan {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'insurance_company_id', type: 'uuid' })
insuranceCompanyId: string;
@ManyToOne(() => InsuranceCompany, (company) => company.plans)
@JoinColumn({ name: 'insurance_company_id' })
insuranceCompany: InsuranceCompany;
@Index()
@Column({ type: 'varchar', length: 50 })
code: string;
@Column({ type: 'varchar', length: 200 })
name: string;
@Column({ type: 'text', nullable: true })
description?: string;
@Column({ name: 'coverage_type', type: 'enum', enum: ['individual', 'family', 'group', 'medicare', 'medicaid', 'other'], default: 'individual' })
coverageType: CoverageType;
@Column({ name: 'coverage_percentage', type: 'jsonb', nullable: true })
coveragePercentage?: CoverageDetails;
@Column({ type: 'jsonb', nullable: true })
deductible?: DeductibleInfo;
@Column({ type: 'jsonb', nullable: true })
copay?: CopayInfo;
@Column({ name: 'out_of_pocket_max', type: 'jsonb', nullable: true })
outOfPocketMax?: OutOfPocketMax;
@Column({ name: 'coinsurance_percentage', type: 'decimal', precision: 5, scale: 2, nullable: true })
coinsurancePercentage?: number;
@Column({ name: 'annual_benefit_max', type: 'decimal', precision: 12, scale: 2, nullable: true })
annualBenefitMax?: number;
@Column({ name: 'lifetime_benefit_max', type: 'decimal', precision: 15, scale: 2, nullable: true })
lifetimeBenefitMax?: number;
@Column({ name: 'waiting_period_days', type: 'int', default: 0 })
waitingPeriodDays: number;
@Column({ name: 'preauthorization_required', type: 'boolean', default: false })
preauthorizationRequired: boolean;
@Column({ name: 'referral_required', type: 'boolean', default: false })
referralRequired: boolean;
@Column({ name: 'network_restrictions', type: 'text', nullable: true })
networkRestrictions?: string;
@Column({ name: 'exclusions', type: 'text', nullable: true })
exclusions?: string;
@Column({ name: 'effective_date', type: 'date', nullable: true })
effectiveDate?: Date;
@Column({ name: 'termination_date', type: 'date', nullable: true })
terminationDate?: Date;
@Column({ type: 'enum', enum: ['active', 'inactive', 'discontinued'], default: 'active' })
status: InsurancePlanStatus;
@Column({ type: 'text', nullable: true })
notes?: string;
@OneToMany(() => PatientInsurance, (patientInsurance) => patientInsurance.insurancePlan)
patientPolicies?: PatientInsurance[];
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@DeleteDateColumn({ name: 'deleted_at', type: 'timestamptz', nullable: true })
deletedAt?: Date;
}

View File

@ -0,0 +1,132 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { InsurancePlan } from './insurance-plan.entity';
export type PatientInsuranceStatus = 'active' | 'inactive' | 'pending_verification' | 'expired' | 'terminated';
export type InsurancePriority = 'primary' | 'secondary' | 'tertiary';
export type RelationshipToSubscriber = 'self' | 'spouse' | 'child' | 'dependent' | 'other';
export interface SubscriberInfo {
firstName?: string;
lastName?: string;
dateOfBirth?: string;
gender?: string;
address?: string;
phone?: string;
ssn?: string;
employerId?: string;
employerName?: string;
}
export interface DeductibleUsage {
usedAmount: number;
remainingAmount: number;
resetDate?: string;
lastUpdated: string;
}
export interface BenefitUsage {
usedAmount: number;
remainingAmount: number;
resetDate?: string;
lastUpdated: string;
}
@Entity({ name: 'patient_insurance', schema: 'clinica' })
export class PatientInsurance {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'patient_id', type: 'uuid' })
patientId: string;
@Index()
@Column({ name: 'insurance_plan_id', type: 'uuid' })
insurancePlanId: string;
@ManyToOne(() => InsurancePlan, (plan) => plan.patientPolicies)
@JoinColumn({ name: 'insurance_plan_id' })
insurancePlan: InsurancePlan;
@Column({ name: 'policy_number', type: 'varchar', length: 100 })
policyNumber: string;
@Column({ name: 'group_number', type: 'varchar', length: 100, nullable: true })
groupNumber?: string;
@Column({ name: 'member_id', type: 'varchar', length: 100, nullable: true })
memberId?: string;
@Column({ type: 'enum', enum: ['primary', 'secondary', 'tertiary'], default: 'primary' })
priority: InsurancePriority;
@Column({ name: 'relationship_to_subscriber', type: 'enum', enum: ['self', 'spouse', 'child', 'dependent', 'other'], default: 'self' })
relationshipToSubscriber: RelationshipToSubscriber;
@Column({ name: 'subscriber_info', type: 'jsonb', nullable: true })
subscriberInfo?: SubscriberInfo;
@Column({ name: 'effective_date', type: 'date' })
effectiveDate: Date;
@Column({ name: 'termination_date', type: 'date', nullable: true })
terminationDate?: Date;
@Column({ name: 'deductible_usage', type: 'jsonb', nullable: true })
deductibleUsage?: DeductibleUsage;
@Column({ name: 'out_of_pocket_usage', type: 'jsonb', nullable: true })
outOfPocketUsage?: BenefitUsage;
@Column({ name: 'benefit_usage', type: 'jsonb', nullable: true })
benefitUsage?: BenefitUsage;
@Column({ name: 'copay_amount', type: 'decimal', precision: 10, scale: 2, nullable: true })
copayAmount?: number;
@Column({ name: 'coinsurance_percentage', type: 'decimal', precision: 5, scale: 2, nullable: true })
coinsurancePercentage?: number;
@Column({ name: 'verification_date', type: 'timestamptz', nullable: true })
verificationDate?: Date;
@Column({ name: 'verified_by', type: 'uuid', nullable: true })
verifiedBy?: string;
@Column({ name: 'verification_reference', type: 'varchar', length: 100, nullable: true })
verificationReference?: string;
@Column({ name: 'is_verified', type: 'boolean', default: false })
isVerified: boolean;
@Column({ name: 'card_front_url', type: 'varchar', length: 500, nullable: true })
cardFrontUrl?: string;
@Column({ name: 'card_back_url', type: 'varchar', length: 500, nullable: true })
cardBackUrl?: string;
@Column({ type: 'enum', enum: ['active', 'inactive', 'pending_verification', 'expired', 'terminated'], default: 'pending_verification' })
status: PatientInsuranceStatus;
@Column({ type: 'text', nullable: true })
notes?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,162 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
Index,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { PatientInsurance } from './patient-insurance.entity';
export type PreauthorizationStatus = 'draft' | 'submitted' | 'pending' | 'approved' | 'denied' | 'expired' | 'cancelled';
export type PreauthorizationType = 'procedure' | 'admission' | 'service' | 'medication' | 'equipment' | 'referral';
export type UrgencyLevel = 'routine' | 'urgent' | 'emergent';
export interface RequestedService {
procedureCode?: string;
procedureDescription: string;
diagnosisCodes?: string[];
quantity?: number;
estimatedCost?: number;
justification?: string;
}
export interface ApprovalDetails {
approvedUnits?: number;
approvedAmount?: number;
approvedProcedures?: string[];
approvedDateRange?: {
from: string;
to: string;
};
approvedFacilities?: string[];
approvedProviders?: string[];
conditions?: string[];
}
export interface DenialDetails {
reasonCode?: string;
reasonDescription?: string;
appealInstructions?: string;
appealDeadline?: string;
alternativesToConcider?: string[];
}
export interface StatusHistory {
status: PreauthorizationStatus;
changedAt: string;
changedBy?: string;
notes?: string;
}
@Entity({ name: 'preauthorizations', schema: 'clinica' })
export class Preauthorization {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index()
@Column({ name: 'tenant_id', type: 'uuid' })
tenantId: string;
@Index()
@Column({ name: 'auth_number', type: 'varchar', length: 50 })
authNumber: string;
@Index()
@Column({ name: 'patient_id', type: 'uuid' })
patientId: string;
@Index()
@Column({ name: 'patient_insurance_id', type: 'uuid' })
patientInsuranceId: string;
@ManyToOne(() => PatientInsurance)
@JoinColumn({ name: 'patient_insurance_id' })
patientInsurance: PatientInsurance;
@Column({ name: 'consultation_id', type: 'uuid', nullable: true })
consultationId?: string;
@Column({ name: 'requesting_provider_id', type: 'uuid' })
requestingProviderId: string;
@Column({ name: 'auth_type', type: 'enum', enum: ['procedure', 'admission', 'service', 'medication', 'equipment', 'referral'], default: 'procedure' })
authType: PreauthorizationType;
@Column({ type: 'enum', enum: ['draft', 'submitted', 'pending', 'approved', 'denied', 'expired', 'cancelled'], default: 'draft' })
status: PreauthorizationStatus;
@Column({ type: 'enum', enum: ['routine', 'urgent', 'emergent'], default: 'routine' })
urgency: UrgencyLevel;
@Column({ name: 'requested_services', type: 'jsonb' })
requestedServices: RequestedService[];
@Column({ name: 'diagnosis_codes', type: 'jsonb', nullable: true })
diagnosisCodes?: string[];
@Column({ name: 'primary_diagnosis_code', type: 'varchar', length: 20, nullable: true })
primaryDiagnosisCode?: string;
@Column({ name: 'clinical_justification', type: 'text', nullable: true })
clinicalJustification?: string;
@Column({ name: 'supporting_documentation_urls', type: 'jsonb', nullable: true })
supportingDocumentationUrls?: string[];
@Column({ name: 'requested_date_from', type: 'date' })
requestedDateFrom: Date;
@Column({ name: 'requested_date_to', type: 'date' })
requestedDateTo: Date;
@Column({ name: 'submitted_at', type: 'timestamptz', nullable: true })
submittedAt?: Date;
@Column({ name: 'submitted_by', type: 'uuid', nullable: true })
submittedBy?: string;
@Column({ name: 'payer_auth_number', type: 'varchar', length: 100, nullable: true })
payerAuthNumber?: string;
@Column({ name: 'payer_contact_name', type: 'varchar', length: 200, nullable: true })
payerContactName?: string;
@Column({ name: 'payer_reference_number', type: 'varchar', length: 100, nullable: true })
payerReferenceNumber?: string;
@Column({ name: 'decision_date', type: 'timestamptz', nullable: true })
decisionDate?: Date;
@Column({ name: 'effective_date', type: 'date', nullable: true })
effectiveDate?: Date;
@Column({ name: 'expiration_date', type: 'date', nullable: true })
expirationDate?: Date;
@Column({ name: 'approval_details', type: 'jsonb', nullable: true })
approvalDetails?: ApprovalDetails;
@Column({ name: 'denial_details', type: 'jsonb', nullable: true })
denialDetails?: DenialDetails;
@Column({ name: 'status_history', type: 'jsonb', nullable: true })
statusHistory?: StatusHistory[];
@Column({ name: 'follow_up_date', type: 'date', nullable: true })
followUpDate?: Date;
@Column({ name: 'follow_up_notes', type: 'text', nullable: true })
followUpNotes?: string;
@Column({ type: 'text', nullable: true })
notes?: string;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,86 @@
export { InsuranceModule, InsuranceModuleOptions } from './insurance.module';
export {
InsuranceCompany,
InsuranceCompanyStatus,
InsuranceCompanyType,
ContactInfo,
AddressInfo,
InsurancePlan,
InsurancePlanStatus,
CoverageType,
CoverageDetails,
DeductibleInfo,
CopayInfo,
OutOfPocketMax,
PatientInsurance,
PatientInsuranceStatus,
InsurancePriority,
RelationshipToSubscriber,
SubscriberInfo,
DeductibleUsage,
BenefitUsage,
InsuranceClaim,
ClaimStatus,
ClaimType,
ClaimLineItem,
EOBInfo,
DenialInfo,
ClaimAdjustment,
Preauthorization,
PreauthorizationStatus,
PreauthorizationType,
UrgencyLevel,
RequestedService,
ApprovalDetails,
DenialDetails,
StatusHistory,
} from './entities';
export {
InsuranceCompanyService,
InsurancePlanService,
PatientInsuranceService,
ClaimService,
PreauthorizationService,
} from './services';
export { InsuranceController } from './controllers';
export {
CreateInsuranceCompanyDto,
UpdateInsuranceCompanyDto,
InsuranceCompanyQueryDto,
CreateInsurancePlanDto,
UpdateInsurancePlanDto,
InsurancePlanQueryDto,
CreatePatientInsuranceDto,
UpdatePatientInsuranceDto,
VerifyInsuranceDto,
PatientInsuranceQueryDto,
VerifyCoverageDto,
CoverageVerificationResult,
CreateInsuranceClaimDto,
UpdateInsuranceClaimDto,
SubmitClaimDto,
ProcessClaimDto,
RecordEOBDto,
VoidClaimDto,
InsuranceClaimQueryDto,
CreatePreauthorizationDto,
UpdatePreauthorizationDto,
SubmitPreauthorizationDto,
ProcessPreauthorizationDto,
CancelPreauthorizationDto,
PreauthorizationQueryDto,
ContactInfoDto,
AddressInfoDto,
CoverageDetailsDto,
DeductibleInfoDto,
CopayInfoDto,
OutOfPocketMaxDto,
SubscriberInfoDto,
ClaimLineItemDto,
RequestedServiceDto,
ApprovalDetailsDto,
} from './dto';

View File

@ -0,0 +1,19 @@
import { Router } from 'express';
import { DataSource } from 'typeorm';
import { InsuranceController } from './controllers';
export interface InsuranceModuleOptions {
dataSource: DataSource;
basePath?: string;
}
export class InsuranceModule {
public router: Router;
private controller: InsuranceController;
constructor(options: InsuranceModuleOptions) {
const { dataSource, basePath = '/api' } = options;
this.controller = new InsuranceController(dataSource, basePath);
this.router = this.controller.router;
}
}

View File

@ -0,0 +1,370 @@
import { DataSource, Repository } from 'typeorm';
import { randomUUID } from 'crypto';
import { InsuranceClaim, PatientInsurance, ClaimStatus } from '../entities';
import {
CreateInsuranceClaimDto,
UpdateInsuranceClaimDto,
SubmitClaimDto,
ProcessClaimDto,
RecordEOBDto,
VoidClaimDto,
InsuranceClaimQueryDto,
} from '../dto';
export class ClaimService {
private repository: Repository<InsuranceClaim>;
private patientInsuranceRepository: Repository<PatientInsurance>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(InsuranceClaim);
this.patientInsuranceRepository = dataSource.getRepository(PatientInsurance);
}
async findAll(tenantId: string, query: InsuranceClaimQueryDto): Promise<{ data: InsuranceClaim[]; total: number }> {
const { patientId, patientInsuranceId, status, claimType, dateFrom, dateTo, page = 1, limit = 20 } = query;
const queryBuilder = this.repository.createQueryBuilder('claim')
.leftJoinAndSelect('claim.patientInsurance', 'pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.leftJoinAndSelect('plan.insuranceCompany', 'company')
.where('claim.tenant_id = :tenantId', { tenantId });
if (patientId) {
queryBuilder.andWhere('claim.patient_id = :patientId', { patientId });
}
if (patientInsuranceId) {
queryBuilder.andWhere('claim.patient_insurance_id = :patientInsuranceId', { patientInsuranceId });
}
if (status) {
queryBuilder.andWhere('claim.status = :status', { status });
}
if (claimType) {
queryBuilder.andWhere('claim.claim_type = :claimType', { claimType });
}
if (dateFrom) {
queryBuilder.andWhere('claim.service_date_from >= :dateFrom', { dateFrom });
}
if (dateTo) {
queryBuilder.andWhere('claim.service_date_to <= :dateTo', { dateTo });
}
queryBuilder
.orderBy('claim.created_at', 'DESC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return { data, total };
}
async findById(tenantId: string, id: string): Promise<InsuranceClaim | null> {
return this.repository.findOne({
where: { id, tenantId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan', 'patientInsurance.insurancePlan.insuranceCompany'],
});
}
async findByClaimNumber(tenantId: string, claimNumber: string): Promise<InsuranceClaim | null> {
return this.repository.findOne({
where: { claimNumber, tenantId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan'],
});
}
async findByPatient(tenantId: string, patientId: string, limit: number = 10): Promise<InsuranceClaim[]> {
return this.repository.find({
where: { tenantId, patientId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan', 'patientInsurance.insurancePlan.insuranceCompany'],
order: { createdAt: 'DESC' },
take: limit,
});
}
async create(tenantId: string, dto: CreateInsuranceClaimDto): Promise<InsuranceClaim> {
const patientInsurance = await this.patientInsuranceRepository.findOne({
where: { id: dto.patientInsuranceId, tenantId },
});
if (!patientInsurance) {
throw new Error('Patient insurance not found');
}
const claimNumber = await this.generateClaimNumber(tenantId);
const lineItemsWithIds = dto.lineItems.map(item => ({
...item,
id: randomUUID(),
}));
const totalChargeAmount = lineItemsWithIds.reduce(
(sum, item) => sum + (item.chargeAmount * item.units),
0
);
const claim = this.repository.create({
tenantId,
claimNumber,
patientId: dto.patientId,
patientInsuranceId: dto.patientInsuranceId,
consultationId: dto.consultationId,
preauthorizationId: dto.preauthorizationId,
claimType: dto.claimType || 'professional',
serviceDateFrom: dto.serviceDateFrom as any,
serviceDateTo: dto.serviceDateTo as any,
diagnosisCodes: dto.diagnosisCodes,
primaryDiagnosisCode: dto.primaryDiagnosisCode,
lineItems: lineItemsWithIds,
totalChargeAmount,
billingProviderId: dto.billingProviderId,
renderingProviderId: dto.renderingProviderId,
placeOfService: dto.placeOfService,
filingDeadline: dto.filingDeadline as any,
isSecondaryClaim: dto.isSecondaryClaim || false,
primaryClaimId: dto.primaryClaimId,
notes: dto.notes,
status: 'draft',
});
return this.repository.save(claim);
}
async update(tenantId: string, id: string, dto: UpdateInsuranceClaimDto): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
if (claim.status !== 'draft') {
throw new Error('Can only update claims in draft status');
}
if (dto.lineItems) {
const lineItemsWithIds = dto.lineItems.map(item => ({
...item,
id: randomUUID(),
}));
claim.lineItems = lineItemsWithIds;
claim.totalChargeAmount = lineItemsWithIds.reduce(
(sum, item) => sum + (item.chargeAmount * item.units),
0
);
}
if (dto.diagnosisCodes) claim.diagnosisCodes = dto.diagnosisCodes;
if (dto.primaryDiagnosisCode) claim.primaryDiagnosisCode = dto.primaryDiagnosisCode;
if (dto.billingProviderId) claim.billingProviderId = dto.billingProviderId;
if (dto.renderingProviderId) claim.renderingProviderId = dto.renderingProviderId;
if (dto.placeOfService) claim.placeOfService = dto.placeOfService;
if (dto.notes !== undefined) claim.notes = dto.notes;
return this.repository.save(claim);
}
async submit(tenantId: string, id: string, dto: SubmitClaimDto): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
if (claim.status !== 'draft') {
throw new Error('Can only submit claims in draft status');
}
claim.status = 'submitted';
claim.submittedAt = new Date();
claim.submittedBy = dto.submittedBy;
this.addAdjustment(claim, 'correction', 'Claim submitted', dto.submittedBy, 'draft', 'submitted');
return this.repository.save(claim);
}
async process(tenantId: string, id: string, dto: ProcessClaimDto): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
if (!['submitted', 'pending', 'in_review'].includes(claim.status)) {
throw new Error('Can only process claims in submitted, pending, or in_review status');
}
const previousStatus = claim.status;
claim.status = dto.decision as ClaimStatus;
claim.totalAllowedAmount = dto.allowedAmount;
claim.totalPaidAmount = dto.paidAmount;
claim.totalDeniedAmount = dto.deniedAmount;
claim.totalAdjustmentAmount = dto.adjustmentAmount;
claim.patientResponsibility = dto.patientResponsibility;
claim.deductibleApplied = dto.deductibleApplied;
claim.copayApplied = dto.copayApplied;
claim.coinsuranceApplied = dto.coinsuranceApplied;
if (dto.payerClaimNumber) {
claim.payerClaimNumber = dto.payerClaimNumber;
}
if (dto.decision === 'denied') {
claim.denialInfo = {
reasonCode: dto.denialReasonCode,
reasonDescription: dto.denialReasonDescription,
};
}
if (dto.notes) {
claim.notes = claim.notes
? `${claim.notes}\n\nProcessing note: ${dto.notes}`
: `Processing note: ${dto.notes}`;
}
this.addAdjustment(claim, 'correction', `Claim processed: ${dto.decision}`, '', previousStatus, claim.status);
return this.repository.save(claim);
}
async recordEOB(tenantId: string, id: string, dto: RecordEOBDto): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
claim.eobInfo = {
receivedDate: dto.receivedDate,
processedDate: dto.processedDate,
checkNumber: dto.checkNumber,
checkDate: dto.checkDate,
eobDocumentUrl: dto.eobDocumentUrl,
paymentMethod: dto.paymentMethod,
remarkCodes: dto.remarkCodes,
adjustmentReasonCodes: dto.adjustmentReasonCodes,
};
if (claim.status === 'approved' || claim.status === 'partial') {
claim.status = 'paid';
}
return this.repository.save(claim);
}
async appeal(tenantId: string, id: string, notes: string): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
if (claim.status !== 'denied') {
throw new Error('Can only appeal denied claims');
}
const previousStatus = claim.status;
claim.status = 'appealed';
claim.notes = claim.notes
? `${claim.notes}\n\nAppeal: ${notes}`
: `Appeal: ${notes}`;
this.addAdjustment(claim, 'resubmission', `Claim appealed: ${notes}`, '', previousStatus, 'appealed');
return this.repository.save(claim);
}
async void(tenantId: string, id: string, dto: VoidClaimDto): Promise<InsuranceClaim | null> {
const claim = await this.findById(tenantId, id);
if (!claim) return null;
if (claim.status === 'paid') {
throw new Error('Cannot void paid claims');
}
const previousStatus = claim.status;
claim.status = 'voided';
this.addAdjustment(claim, 'void', dto.reason, dto.voidedBy, previousStatus, 'voided');
return this.repository.save(claim);
}
async getClaimsByStatus(tenantId: string): Promise<{ status: string; count: number; totalAmount: number }[]> {
const result = await this.repository.createQueryBuilder('claim')
.select('claim.status', 'status')
.addSelect('COUNT(*)', 'count')
.addSelect('COALESCE(SUM(claim.total_charge_amount), 0)', 'totalAmount')
.where('claim.tenant_id = :tenantId', { tenantId })
.groupBy('claim.status')
.getRawMany();
return result.map(r => ({
status: r.status,
count: parseInt(r.count, 10),
totalAmount: parseFloat(r.totalAmount),
}));
}
async getPendingClaimsCount(tenantId: string): Promise<number> {
return this.repository.count({
where: { tenantId, status: 'pending' },
});
}
async getAgingReport(tenantId: string): Promise<{ ageRange: string; count: number; totalAmount: number }[]> {
const result = await this.repository.createQueryBuilder('claim')
.select(`
CASE
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '30 days' THEN '0-30 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '60 days' THEN '31-60 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '90 days' THEN '61-90 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '120 days' THEN '91-120 days'
ELSE '120+ days'
END
`, 'ageRange')
.addSelect('COUNT(*)', 'count')
.addSelect('COALESCE(SUM(claim.total_charge_amount), 0)', 'totalAmount')
.where('claim.tenant_id = :tenantId', { tenantId })
.andWhere('claim.status IN (:...statuses)', { statuses: ['submitted', 'pending', 'in_review'] })
.andWhere('claim.submitted_at IS NOT NULL')
.groupBy(`
CASE
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '30 days' THEN '0-30 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '60 days' THEN '31-60 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '90 days' THEN '61-90 days'
WHEN claim.submitted_at >= CURRENT_DATE - INTERVAL '120 days' THEN '91-120 days'
ELSE '120+ days'
END
`)
.getRawMany();
return result.map(r => ({
ageRange: r.ageRange,
count: parseInt(r.count, 10),
totalAmount: parseFloat(r.totalAmount),
}));
}
private addAdjustment(
claim: InsuranceClaim,
type: 'correction' | 'void' | 'resubmission',
reason: string,
adjustedBy: string,
previousStatus: ClaimStatus,
newStatus: ClaimStatus
): void {
claim.adjustments = claim.adjustments || [];
claim.adjustments.push({
date: new Date().toISOString(),
type,
reason,
adjustedBy,
previousStatus,
newStatus,
});
}
private async generateClaimNumber(tenantId: string): Promise<string> {
const today = new Date();
const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
const count = await this.repository.createQueryBuilder('claim')
.where('claim.tenant_id = :tenantId', { tenantId })
.andWhere('DATE(claim.created_at) = CURRENT_DATE')
.getCount();
const sequence = (count + 1).toString().padStart(4, '0');
return `CLM-${dateStr}-${sequence}`;
}
}

View File

@ -0,0 +1,5 @@
export { InsuranceCompanyService } from './insurance-company.service';
export { InsurancePlanService } from './insurance-plan.service';
export { PatientInsuranceService } from './patient-insurance.service';
export { ClaimService } from './claim.service';
export { PreauthorizationService } from './preauthorization.service';

View File

@ -0,0 +1,112 @@
import { DataSource, Repository } from 'typeorm';
import { InsuranceCompany } from '../entities';
import { CreateInsuranceCompanyDto, UpdateInsuranceCompanyDto, InsuranceCompanyQueryDto } from '../dto';
export class InsuranceCompanyService {
private repository: Repository<InsuranceCompany>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(InsuranceCompany);
}
async findAll(tenantId: string, query: InsuranceCompanyQueryDto): Promise<{ data: InsuranceCompany[]; total: number }> {
const { search, type, status, page = 1, limit = 50 } = query;
const queryBuilder = this.repository.createQueryBuilder('company')
.where('company.tenant_id = :tenantId', { tenantId })
.andWhere('company.deleted_at IS NULL');
if (type) {
queryBuilder.andWhere('company.type = :type', { type });
}
if (status) {
queryBuilder.andWhere('company.status = :status', { status });
}
if (search) {
queryBuilder.andWhere(
'(company.code ILIKE :search OR company.name ILIKE :search OR company.legal_name ILIKE :search)',
{ search: `%${search}%` }
);
}
queryBuilder
.orderBy('company.name', 'ASC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return { data, total };
}
async findById(tenantId: string, id: string): Promise<InsuranceCompany | null> {
return this.repository.findOne({
where: { id, tenantId },
relations: ['plans'],
});
}
async findByCode(tenantId: string, code: string): Promise<InsuranceCompany | null> {
return this.repository.findOne({
where: { code, tenantId },
});
}
async findActive(tenantId: string): Promise<InsuranceCompany[]> {
return this.repository.find({
where: { tenantId, status: 'active' },
order: { name: 'ASC' },
});
}
async create(tenantId: string, dto: CreateInsuranceCompanyDto): Promise<InsuranceCompany> {
const existing = await this.findByCode(tenantId, dto.code);
if (existing) {
throw new Error(`Insurance company with code ${dto.code} already exists`);
}
const company = this.repository.create({
...dto,
tenantId,
});
return this.repository.save(company);
}
async update(tenantId: string, id: string, dto: UpdateInsuranceCompanyDto): Promise<InsuranceCompany | null> {
const company = await this.findById(tenantId, id);
if (!company) return null;
if (dto.code && dto.code !== company.code) {
const existing = await this.findByCode(tenantId, dto.code);
if (existing) {
throw new Error(`Insurance company with code ${dto.code} already exists`);
}
}
Object.assign(company, dto);
return this.repository.save(company);
}
async softDelete(tenantId: string, id: string): Promise<boolean> {
const company = await this.findById(tenantId, id);
if (!company) return false;
await this.repository.softDelete(id);
return true;
}
async getByType(tenantId: string): Promise<{ type: string; count: number }[]> {
const result = await this.repository.createQueryBuilder('company')
.select('company.type', 'type')
.addSelect('COUNT(*)', 'count')
.where('company.tenant_id = :tenantId', { tenantId })
.andWhere('company.deleted_at IS NULL')
.andWhere('company.status = :status', { status: 'active' })
.groupBy('company.type')
.orderBy('company.type', 'ASC')
.getRawMany();
return result.map(r => ({ type: r.type, count: parseInt(r.count, 10) }));
}
}

View File

@ -0,0 +1,137 @@
import { DataSource, Repository } from 'typeorm';
import { InsurancePlan, InsuranceCompany } from '../entities';
import { CreateInsurancePlanDto, UpdateInsurancePlanDto, InsurancePlanQueryDto } from '../dto';
export class InsurancePlanService {
private repository: Repository<InsurancePlan>;
private companyRepository: Repository<InsuranceCompany>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(InsurancePlan);
this.companyRepository = dataSource.getRepository(InsuranceCompany);
}
async findAll(tenantId: string, query: InsurancePlanQueryDto): Promise<{ data: InsurancePlan[]; total: number }> {
const { search, insuranceCompanyId, coverageType, status, page = 1, limit = 50 } = query;
const queryBuilder = this.repository.createQueryBuilder('plan')
.leftJoinAndSelect('plan.insuranceCompany', 'company')
.where('plan.tenant_id = :tenantId', { tenantId })
.andWhere('plan.deleted_at IS NULL');
if (insuranceCompanyId) {
queryBuilder.andWhere('plan.insurance_company_id = :insuranceCompanyId', { insuranceCompanyId });
}
if (coverageType) {
queryBuilder.andWhere('plan.coverage_type = :coverageType', { coverageType });
}
if (status) {
queryBuilder.andWhere('plan.status = :status', { status });
}
if (search) {
queryBuilder.andWhere(
'(plan.code ILIKE :search OR plan.name ILIKE :search)',
{ search: `%${search}%` }
);
}
queryBuilder
.orderBy('company.name', 'ASC')
.addOrderBy('plan.name', 'ASC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return { data, total };
}
async findById(tenantId: string, id: string): Promise<InsurancePlan | null> {
return this.repository.findOne({
where: { id, tenantId },
relations: ['insuranceCompany'],
});
}
async findByCode(tenantId: string, code: string): Promise<InsurancePlan | null> {
return this.repository.findOne({
where: { code, tenantId },
relations: ['insuranceCompany'],
});
}
async findByCompany(tenantId: string, insuranceCompanyId: string): Promise<InsurancePlan[]> {
return this.repository.find({
where: { tenantId, insuranceCompanyId, status: 'active' },
order: { name: 'ASC' },
});
}
async findActive(tenantId: string): Promise<InsurancePlan[]> {
return this.repository.find({
where: { tenantId, status: 'active' },
relations: ['insuranceCompany'],
order: { name: 'ASC' },
});
}
async create(tenantId: string, dto: CreateInsurancePlanDto): Promise<InsurancePlan> {
const company = await this.companyRepository.findOne({
where: { id: dto.insuranceCompanyId, tenantId },
});
if (!company) {
throw new Error('Insurance company not found');
}
const existing = await this.findByCode(tenantId, dto.code);
if (existing) {
throw new Error(`Insurance plan with code ${dto.code} already exists`);
}
const plan = this.repository.create({
...dto,
tenantId,
});
return this.repository.save(plan);
}
async update(tenantId: string, id: string, dto: UpdateInsurancePlanDto): Promise<InsurancePlan | null> {
const plan = await this.findById(tenantId, id);
if (!plan) return null;
if (dto.code && dto.code !== plan.code) {
const existing = await this.findByCode(tenantId, dto.code);
if (existing) {
throw new Error(`Insurance plan with code ${dto.code} already exists`);
}
}
Object.assign(plan, dto);
return this.repository.save(plan);
}
async softDelete(tenantId: string, id: string): Promise<boolean> {
const plan = await this.findById(tenantId, id);
if (!plan) return false;
await this.repository.softDelete(id);
return true;
}
async getByCoverageType(tenantId: string): Promise<{ coverageType: string; count: number }[]> {
const result = await this.repository.createQueryBuilder('plan')
.select('plan.coverage_type', 'coverageType')
.addSelect('COUNT(*)', 'count')
.where('plan.tenant_id = :tenantId', { tenantId })
.andWhere('plan.deleted_at IS NULL')
.andWhere('plan.status = :status', { status: 'active' })
.groupBy('plan.coverage_type')
.orderBy('plan.coverage_type', 'ASC')
.getRawMany();
return result.map(r => ({ coverageType: r.coverageType, count: parseInt(r.count, 10) }));
}
}

View File

@ -0,0 +1,271 @@
import { DataSource, Repository } from 'typeorm';
import { PatientInsurance, InsurancePlan } from '../entities';
import {
CreatePatientInsuranceDto,
UpdatePatientInsuranceDto,
VerifyInsuranceDto,
PatientInsuranceQueryDto,
VerifyCoverageDto,
CoverageVerificationResult,
} from '../dto';
export class PatientInsuranceService {
private repository: Repository<PatientInsurance>;
private planRepository: Repository<InsurancePlan>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(PatientInsurance);
this.planRepository = dataSource.getRepository(InsurancePlan);
}
async findAll(tenantId: string, query: PatientInsuranceQueryDto): Promise<{ data: PatientInsurance[]; total: number }> {
const { patientId, insurancePlanId, priority, status, isVerified, page = 1, limit = 50 } = query;
const queryBuilder = this.repository.createQueryBuilder('pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.leftJoinAndSelect('plan.insuranceCompany', 'company')
.where('pi.tenant_id = :tenantId', { tenantId });
if (patientId) {
queryBuilder.andWhere('pi.patient_id = :patientId', { patientId });
}
if (insurancePlanId) {
queryBuilder.andWhere('pi.insurance_plan_id = :insurancePlanId', { insurancePlanId });
}
if (priority) {
queryBuilder.andWhere('pi.priority = :priority', { priority });
}
if (status) {
queryBuilder.andWhere('pi.status = :status', { status });
}
if (isVerified !== undefined) {
queryBuilder.andWhere('pi.is_verified = :isVerified', { isVerified });
}
queryBuilder
.orderBy('pi.priority', 'ASC')
.addOrderBy('pi.created_at', 'DESC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return { data, total };
}
async findById(tenantId: string, id: string): Promise<PatientInsurance | null> {
return this.repository.findOne({
where: { id, tenantId },
relations: ['insurancePlan', 'insurancePlan.insuranceCompany'],
});
}
async findByPatient(tenantId: string, patientId: string): Promise<PatientInsurance[]> {
return this.repository.find({
where: { tenantId, patientId },
relations: ['insurancePlan', 'insurancePlan.insuranceCompany'],
order: { priority: 'ASC' },
});
}
async findActiveByPatient(tenantId: string, patientId: string): Promise<PatientInsurance[]> {
return this.repository.find({
where: { tenantId, patientId, status: 'active' },
relations: ['insurancePlan', 'insurancePlan.insuranceCompany'],
order: { priority: 'ASC' },
});
}
async findPrimaryByPatient(tenantId: string, patientId: string): Promise<PatientInsurance | null> {
return this.repository.findOne({
where: { tenantId, patientId, priority: 'primary', status: 'active' },
relations: ['insurancePlan', 'insurancePlan.insuranceCompany'],
});
}
async create(tenantId: string, dto: CreatePatientInsuranceDto): Promise<PatientInsurance> {
const plan = await this.planRepository.findOne({
where: { id: dto.insurancePlanId, tenantId },
});
if (!plan) {
throw new Error('Insurance plan not found');
}
if (dto.priority === 'primary') {
const existingPrimary = await this.findPrimaryByPatient(tenantId, dto.patientId);
if (existingPrimary) {
existingPrimary.priority = 'secondary';
await this.repository.save(existingPrimary);
}
}
const patientInsurance = this.repository.create({
...dto,
tenantId,
status: 'pending_verification',
});
return this.repository.save(patientInsurance);
}
async update(tenantId: string, id: string, dto: UpdatePatientInsuranceDto): Promise<PatientInsurance | null> {
const patientInsurance = await this.findById(tenantId, id);
if (!patientInsurance) return null;
if (dto.priority === 'primary' && patientInsurance.priority !== 'primary') {
const existingPrimary = await this.findPrimaryByPatient(tenantId, patientInsurance.patientId);
if (existingPrimary && existingPrimary.id !== id) {
existingPrimary.priority = 'secondary';
await this.repository.save(existingPrimary);
}
}
Object.assign(patientInsurance, dto);
return this.repository.save(patientInsurance);
}
async verify(tenantId: string, id: string, dto: VerifyInsuranceDto): Promise<PatientInsurance | null> {
const patientInsurance = await this.findById(tenantId, id);
if (!patientInsurance) return null;
patientInsurance.isVerified = true;
patientInsurance.verificationDate = new Date();
patientInsurance.verifiedBy = dto.verifiedBy;
patientInsurance.verificationReference = dto.verificationReference;
patientInsurance.status = 'active';
if (dto.notes) {
patientInsurance.notes = patientInsurance.notes
? `${patientInsurance.notes}\n\nVerification note: ${dto.notes}`
: `Verification note: ${dto.notes}`;
}
return this.repository.save(patientInsurance);
}
async terminate(tenantId: string, id: string, terminationDate?: Date): Promise<PatientInsurance | null> {
const patientInsurance = await this.findById(tenantId, id);
if (!patientInsurance) return null;
patientInsurance.status = 'terminated';
patientInsurance.terminationDate = terminationDate || new Date();
return this.repository.save(patientInsurance);
}
async verifyCoverage(tenantId: string, dto: VerifyCoverageDto): Promise<CoverageVerificationResult> {
const patientInsurance = await this.findById(tenantId, dto.patientInsuranceId);
if (!patientInsurance || !patientInsurance.insurancePlan) {
return {
isCovered: false,
preauthorizationRequired: false,
referralRequired: false,
limitations: ['Insurance policy not found or inactive'],
};
}
if (patientInsurance.status !== 'active') {
return {
isCovered: false,
preauthorizationRequired: false,
referralRequired: false,
limitations: [`Insurance status: ${patientInsurance.status}`],
};
}
const plan = patientInsurance.insurancePlan;
const coveragePercentage = plan.coveragePercentage?.consultations || 80;
const copay = plan.copay?.specialist || patientInsurance.copayAmount || 0;
const coinsurance = patientInsurance.coinsurancePercentage || plan.coinsurancePercentage || 20;
const deductibleRemaining = patientInsurance.deductibleUsage?.remainingAmount;
const outOfPocketRemaining = patientInsurance.outOfPocketUsage?.remainingAmount;
let estimatedCopay: number | undefined;
let estimatedCoinsurance: number | undefined;
if (dto.estimatedAmount) {
estimatedCopay = copay;
estimatedCoinsurance = (dto.estimatedAmount - copay) * (coinsurance / 100);
}
return {
isCovered: true,
coveragePercentage,
estimatedCopay,
estimatedCoinsurance,
deductibleRemaining,
outOfPocketRemaining,
preauthorizationRequired: plan.preauthorizationRequired,
referralRequired: plan.referralRequired,
networkStatus: 'in_network',
limitations: plan.networkRestrictions ? [plan.networkRestrictions] : undefined,
exclusions: plan.exclusions ? [plan.exclusions] : undefined,
};
}
async updateDeductibleUsage(
tenantId: string,
id: string,
usedAmount: number
): Promise<PatientInsurance | null> {
const patientInsurance = await this.findById(tenantId, id);
if (!patientInsurance) return null;
const plan = patientInsurance.insurancePlan;
const deductibleMax = plan?.deductible?.individual || 0;
patientInsurance.deductibleUsage = {
usedAmount: (patientInsurance.deductibleUsage?.usedAmount || 0) + usedAmount,
remainingAmount: Math.max(0, deductibleMax - ((patientInsurance.deductibleUsage?.usedAmount || 0) + usedAmount)),
lastUpdated: new Date().toISOString(),
};
return this.repository.save(patientInsurance);
}
async updateOutOfPocketUsage(
tenantId: string,
id: string,
usedAmount: number
): Promise<PatientInsurance | null> {
const patientInsurance = await this.findById(tenantId, id);
if (!patientInsurance) return null;
const plan = patientInsurance.insurancePlan;
const oopMax = plan?.outOfPocketMax?.individual || 0;
patientInsurance.outOfPocketUsage = {
usedAmount: (patientInsurance.outOfPocketUsage?.usedAmount || 0) + usedAmount,
remainingAmount: Math.max(0, oopMax - ((patientInsurance.outOfPocketUsage?.usedAmount || 0) + usedAmount)),
lastUpdated: new Date().toISOString(),
};
return this.repository.save(patientInsurance);
}
async getUnverifiedCount(tenantId: string): Promise<number> {
return this.repository.count({
where: { tenantId, isVerified: false, status: 'pending_verification' },
});
}
async getExpiringPolicies(tenantId: string, daysAhead: number = 30): Promise<PatientInsurance[]> {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + daysAhead);
return this.repository.createQueryBuilder('pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.leftJoinAndSelect('plan.insuranceCompany', 'company')
.where('pi.tenant_id = :tenantId', { tenantId })
.andWhere('pi.status = :status', { status: 'active' })
.andWhere('pi.termination_date IS NOT NULL')
.andWhere('pi.termination_date <= :futureDate', { futureDate })
.andWhere('pi.termination_date >= CURRENT_DATE')
.orderBy('pi.termination_date', 'ASC')
.getMany();
}
}

View File

@ -0,0 +1,347 @@
import { DataSource, Repository } from 'typeorm';
import { Preauthorization, PatientInsurance, PreauthorizationStatus } from '../entities';
import {
CreatePreauthorizationDto,
UpdatePreauthorizationDto,
SubmitPreauthorizationDto,
ProcessPreauthorizationDto,
CancelPreauthorizationDto,
PreauthorizationQueryDto,
} from '../dto';
export class PreauthorizationService {
private repository: Repository<Preauthorization>;
private patientInsuranceRepository: Repository<PatientInsurance>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(Preauthorization);
this.patientInsuranceRepository = dataSource.getRepository(PatientInsurance);
}
async findAll(tenantId: string, query: PreauthorizationQueryDto): Promise<{ data: Preauthorization[]; total: number }> {
const { patientId, patientInsuranceId, status, authType, urgency, dateFrom, dateTo, page = 1, limit = 20 } = query;
const queryBuilder = this.repository.createQueryBuilder('preauth')
.leftJoinAndSelect('preauth.patientInsurance', 'pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.leftJoinAndSelect('plan.insuranceCompany', 'company')
.where('preauth.tenant_id = :tenantId', { tenantId });
if (patientId) {
queryBuilder.andWhere('preauth.patient_id = :patientId', { patientId });
}
if (patientInsuranceId) {
queryBuilder.andWhere('preauth.patient_insurance_id = :patientInsuranceId', { patientInsuranceId });
}
if (status) {
queryBuilder.andWhere('preauth.status = :status', { status });
}
if (authType) {
queryBuilder.andWhere('preauth.auth_type = :authType', { authType });
}
if (urgency) {
queryBuilder.andWhere('preauth.urgency = :urgency', { urgency });
}
if (dateFrom) {
queryBuilder.andWhere('preauth.requested_date_from >= :dateFrom', { dateFrom });
}
if (dateTo) {
queryBuilder.andWhere('preauth.requested_date_to <= :dateTo', { dateTo });
}
queryBuilder
.orderBy('preauth.urgency', 'DESC')
.addOrderBy('preauth.created_at', 'DESC')
.skip((page - 1) * limit)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return { data, total };
}
async findById(tenantId: string, id: string): Promise<Preauthorization | null> {
return this.repository.findOne({
where: { id, tenantId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan', 'patientInsurance.insurancePlan.insuranceCompany'],
});
}
async findByAuthNumber(tenantId: string, authNumber: string): Promise<Preauthorization | null> {
return this.repository.findOne({
where: { authNumber, tenantId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan'],
});
}
async findByPatient(tenantId: string, patientId: string, limit: number = 10): Promise<Preauthorization[]> {
return this.repository.find({
where: { tenantId, patientId },
relations: ['patientInsurance', 'patientInsurance.insurancePlan', 'patientInsurance.insurancePlan.insuranceCompany'],
order: { createdAt: 'DESC' },
take: limit,
});
}
async findActive(tenantId: string, patientId: string): Promise<Preauthorization[]> {
return this.repository.createQueryBuilder('preauth')
.leftJoinAndSelect('preauth.patientInsurance', 'pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.where('preauth.tenant_id = :tenantId', { tenantId })
.andWhere('preauth.patient_id = :patientId', { patientId })
.andWhere('preauth.status = :status', { status: 'approved' })
.andWhere('(preauth.expiration_date IS NULL OR preauth.expiration_date >= CURRENT_DATE)')
.orderBy('preauth.expiration_date', 'ASC')
.getMany();
}
async create(tenantId: string, dto: CreatePreauthorizationDto): Promise<Preauthorization> {
const patientInsurance = await this.patientInsuranceRepository.findOne({
where: { id: dto.patientInsuranceId, tenantId },
});
if (!patientInsurance) {
throw new Error('Patient insurance not found');
}
const authNumber = await this.generateAuthNumber(tenantId);
const preauth = this.repository.create({
tenantId,
authNumber,
patientId: dto.patientId,
patientInsuranceId: dto.patientInsuranceId,
consultationId: dto.consultationId,
requestingProviderId: dto.requestingProviderId,
authType: dto.authType || 'procedure',
urgency: dto.urgency || 'routine',
requestedServices: dto.requestedServices,
diagnosisCodes: dto.diagnosisCodes,
primaryDiagnosisCode: dto.primaryDiagnosisCode,
clinicalJustification: dto.clinicalJustification,
supportingDocumentationUrls: dto.supportingDocumentationUrls,
requestedDateFrom: dto.requestedDateFrom as any,
requestedDateTo: dto.requestedDateTo as any,
notes: dto.notes,
status: 'draft',
statusHistory: [{
status: 'draft',
changedAt: new Date().toISOString(),
notes: 'Preauthorization created',
}],
});
return this.repository.save(preauth);
}
async update(tenantId: string, id: string, dto: UpdatePreauthorizationDto): Promise<Preauthorization | null> {
const preauth = await this.findById(tenantId, id);
if (!preauth) return null;
if (!['draft', 'pending'].includes(preauth.status)) {
throw new Error('Can only update preauthorizations in draft or pending status');
}
if (dto.authType) preauth.authType = dto.authType;
if (dto.urgency) preauth.urgency = dto.urgency;
if (dto.requestedServices) preauth.requestedServices = dto.requestedServices;
if (dto.diagnosisCodes) preauth.diagnosisCodes = dto.diagnosisCodes;
if (dto.primaryDiagnosisCode) preauth.primaryDiagnosisCode = dto.primaryDiagnosisCode;
if (dto.clinicalJustification) preauth.clinicalJustification = dto.clinicalJustification;
if (dto.supportingDocumentationUrls) preauth.supportingDocumentationUrls = dto.supportingDocumentationUrls;
if (dto.requestedDateFrom) preauth.requestedDateFrom = dto.requestedDateFrom as any;
if (dto.requestedDateTo) preauth.requestedDateTo = dto.requestedDateTo as any;
if (dto.followUpDate) preauth.followUpDate = dto.followUpDate as any;
if (dto.followUpNotes) preauth.followUpNotes = dto.followUpNotes;
if (dto.notes !== undefined) preauth.notes = dto.notes;
return this.repository.save(preauth);
}
async submit(tenantId: string, id: string, dto: SubmitPreauthorizationDto): Promise<Preauthorization | null> {
const preauth = await this.findById(tenantId, id);
if (!preauth) return null;
if (preauth.status !== 'draft') {
throw new Error('Can only submit preauthorizations in draft status');
}
preauth.status = 'submitted';
preauth.submittedAt = new Date();
preauth.submittedBy = dto.submittedBy;
this.addStatusHistory(preauth, 'submitted', dto.submittedBy, 'Preauthorization submitted');
return this.repository.save(preauth);
}
async markPending(tenantId: string, id: string, payerReferenceNumber?: string): Promise<Preauthorization | null> {
const preauth = await this.findById(tenantId, id);
if (!preauth) return null;
if (preauth.status !== 'submitted') {
throw new Error('Can only mark as pending from submitted status');
}
preauth.status = 'pending';
if (payerReferenceNumber) {
preauth.payerReferenceNumber = payerReferenceNumber;
}
this.addStatusHistory(preauth, 'pending', undefined, 'Awaiting payer decision');
return this.repository.save(preauth);
}
async process(tenantId: string, id: string, dto: ProcessPreauthorizationDto): Promise<Preauthorization | null> {
const preauth = await this.findById(tenantId, id);
if (!preauth) return null;
if (!['submitted', 'pending'].includes(preauth.status)) {
throw new Error('Can only process preauthorizations in submitted or pending status');
}
preauth.decisionDate = new Date();
if (dto.decision === 'approved') {
preauth.status = 'approved';
preauth.payerAuthNumber = dto.payerAuthNumber;
preauth.effectiveDate = dto.effectiveDate as any;
preauth.expirationDate = dto.expirationDate as any;
if (dto.approvalDetails) {
preauth.approvalDetails = {
approvedUnits: dto.approvalDetails.approvedUnits,
approvedAmount: dto.approvalDetails.approvedAmount,
approvedProcedures: dto.approvalDetails.approvedProcedures,
approvedDateRange: dto.approvalDetails.approvedDateFrom && dto.approvalDetails.approvedDateTo
? { from: dto.approvalDetails.approvedDateFrom, to: dto.approvalDetails.approvedDateTo }
: undefined,
approvedFacilities: dto.approvalDetails.approvedFacilities,
approvedProviders: dto.approvalDetails.approvedProviders,
conditions: dto.approvalDetails.conditions,
};
}
this.addStatusHistory(preauth, 'approved', undefined, `Approved. Auth#: ${dto.payerAuthNumber || 'N/A'}`);
} else {
preauth.status = 'denied';
preauth.denialDetails = {
reasonCode: dto.denialReasonCode,
reasonDescription: dto.denialReasonDescription,
appealInstructions: dto.appealInstructions,
appealDeadline: dto.appealDeadline,
};
this.addStatusHistory(preauth, 'denied', undefined, `Denied: ${dto.denialReasonDescription || dto.denialReasonCode || 'No reason provided'}`);
}
if (dto.notes) {
preauth.notes = preauth.notes
? `${preauth.notes}\n\nDecision note: ${dto.notes}`
: `Decision note: ${dto.notes}`;
}
return this.repository.save(preauth);
}
async cancel(tenantId: string, id: string, dto: CancelPreauthorizationDto): Promise<Preauthorization | null> {
const preauth = await this.findById(tenantId, id);
if (!preauth) return null;
if (['approved', 'denied', 'cancelled'].includes(preauth.status)) {
throw new Error('Cannot cancel preauthorizations that are already finalized');
}
preauth.status = 'cancelled';
this.addStatusHistory(preauth, 'cancelled', dto.cancelledBy, dto.reason);
return this.repository.save(preauth);
}
async checkExpired(tenantId: string): Promise<number> {
const result = await this.repository.createQueryBuilder()
.update(Preauthorization)
.set({ status: 'expired' })
.where('tenant_id = :tenantId', { tenantId })
.andWhere('status = :status', { status: 'approved' })
.andWhere('expiration_date < CURRENT_DATE')
.execute();
return result.affected || 0;
}
async getPreauthorizationsByStatus(tenantId: string): Promise<{ status: string; count: number }[]> {
const result = await this.repository.createQueryBuilder('preauth')
.select('preauth.status', 'status')
.addSelect('COUNT(*)', 'count')
.where('preauth.tenant_id = :tenantId', { tenantId })
.groupBy('preauth.status')
.getRawMany();
return result.map(r => ({ status: r.status, count: parseInt(r.count, 10) }));
}
async getPendingCount(tenantId: string): Promise<number> {
return this.repository.count({
where: { tenantId, status: 'pending' },
});
}
async getExpiringPreauthorizations(tenantId: string, daysAhead: number = 14): Promise<Preauthorization[]> {
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + daysAhead);
return this.repository.createQueryBuilder('preauth')
.leftJoinAndSelect('preauth.patientInsurance', 'pi')
.leftJoinAndSelect('pi.insurancePlan', 'plan')
.where('preauth.tenant_id = :tenantId', { tenantId })
.andWhere('preauth.status = :status', { status: 'approved' })
.andWhere('preauth.expiration_date IS NOT NULL')
.andWhere('preauth.expiration_date <= :futureDate', { futureDate })
.andWhere('preauth.expiration_date >= CURRENT_DATE')
.orderBy('preauth.expiration_date', 'ASC')
.getMany();
}
async getUrgentPreauthorizations(tenantId: string): Promise<Preauthorization[]> {
return this.repository.find({
where: { tenantId, urgency: 'urgent', status: 'pending' },
relations: ['patientInsurance', 'patientInsurance.insurancePlan'],
order: { createdAt: 'ASC' },
});
}
private addStatusHistory(
preauth: Preauthorization,
status: PreauthorizationStatus,
changedBy?: string,
notes?: string
): void {
preauth.statusHistory = preauth.statusHistory || [];
preauth.statusHistory.push({
status,
changedAt: new Date().toISOString(),
changedBy,
notes,
});
}
private async generateAuthNumber(tenantId: string): Promise<string> {
const today = new Date();
const dateStr = today.toISOString().slice(0, 10).replace(/-/g, '');
const count = await this.repository.createQueryBuilder('preauth')
.where('preauth.tenant_id = :tenantId', { tenantId })
.andWhere('DATE(preauth.created_at) = CURRENT_DATE')
.getCount();
const sequence = (count + 1).toString().padStart(4, '0');
return `PA-${dateStr}-${sequence}`;
}
}