[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:
parent
e4d915889a
commit
1b38818354
1
src/modules/insurance/controllers/index.ts
Normal file
1
src/modules/insurance/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { InsuranceController } from './insurance.controller';
|
||||
968
src/modules/insurance/controllers/insurance.controller.ts
Normal file
968
src/modules/insurance/controllers/insurance.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1463
src/modules/insurance/dto/index.ts
Normal file
1463
src/modules/insurance/dto/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
48
src/modules/insurance/entities/index.ts
Normal file
48
src/modules/insurance/entities/index.ts
Normal 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';
|
||||
184
src/modules/insurance/entities/insurance-claim.entity.ts
Normal file
184
src/modules/insurance/entities/insurance-claim.entity.ts
Normal 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;
|
||||
}
|
||||
107
src/modules/insurance/entities/insurance-company.entity.ts
Normal file
107
src/modules/insurance/entities/insurance-company.entity.ts
Normal 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;
|
||||
}
|
||||
148
src/modules/insurance/entities/insurance-plan.entity.ts
Normal file
148
src/modules/insurance/entities/insurance-plan.entity.ts
Normal 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;
|
||||
}
|
||||
132
src/modules/insurance/entities/patient-insurance.entity.ts
Normal file
132
src/modules/insurance/entities/patient-insurance.entity.ts
Normal 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;
|
||||
}
|
||||
162
src/modules/insurance/entities/preauthorization.entity.ts
Normal file
162
src/modules/insurance/entities/preauthorization.entity.ts
Normal 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;
|
||||
}
|
||||
86
src/modules/insurance/index.ts
Normal file
86
src/modules/insurance/index.ts
Normal 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';
|
||||
19
src/modules/insurance/insurance.module.ts
Normal file
19
src/modules/insurance/insurance.module.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
370
src/modules/insurance/services/claim.service.ts
Normal file
370
src/modules/insurance/services/claim.service.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
5
src/modules/insurance/services/index.ts
Normal file
5
src/modules/insurance/services/index.ts
Normal 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';
|
||||
112
src/modules/insurance/services/insurance-company.service.ts
Normal file
112
src/modules/insurance/services/insurance-company.service.ts
Normal 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) }));
|
||||
}
|
||||
}
|
||||
137
src/modules/insurance/services/insurance-plan.service.ts
Normal file
137
src/modules/insurance/services/insurance-plan.service.ts
Normal 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) }));
|
||||
}
|
||||
}
|
||||
271
src/modules/insurance/services/patient-insurance.service.ts
Normal file
271
src/modules/insurance/services/patient-insurance.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
347
src/modules/insurance/services/preauthorization.service.ts
Normal file
347
src/modules/insurance/services/preauthorization.service.ts
Normal 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}`;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user