2025-10-06 15:25:37 +03:00

776 lines
23 KiB
Python

"""
Insurance Approvals app models for hospital management system.
Provides universal insurance approval request management for all order types.
"""
import uuid
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.conf import settings
from datetime import timedelta
from decimal import Decimal
class InsuranceApprovalRequest(models.Model):
"""
Universal insurance approval request for any order type.
Uses GenericForeignKey for polymorphic relationships.
"""
class RequestType(models.TextChoices):
LABORATORY = 'LABORATORY', 'Laboratory Test'
RADIOLOGY = 'RADIOLOGY', 'Radiology/Imaging'
PHARMACY = 'PHARMACY', 'Medication/Prescription'
PROCEDURE = 'PROCEDURE', 'Medical Procedure'
SURGERY = 'SURGERY', 'Surgical Procedure'
THERAPY = 'THERAPY', 'Therapy Services'
DME = 'DME', 'Durable Medical Equipment'
HOME_HEALTH = 'HOME_HEALTH', 'Home Health Services'
OTHER = 'OTHER', 'Other Services'
class ApprovalStatus(models.TextChoices):
DRAFT = 'DRAFT', 'Draft'
PENDING_SUBMISSION = 'PENDING_SUBMISSION', 'Pending Submission'
SUBMITTED = 'SUBMITTED', 'Submitted to Insurance'
UNDER_REVIEW = 'UNDER_REVIEW', 'Under Review'
ADDITIONAL_INFO_REQUESTED = 'ADDITIONAL_INFO_REQUESTED', 'Additional Info Requested'
APPROVED = 'APPROVED', 'Approved'
PARTIALLY_APPROVED = 'PARTIALLY_APPROVED', 'Partially Approved'
DENIED = 'DENIED', 'Denied'
APPEALED = 'APPEALED', 'Appealed'
APPEAL_APPROVED = 'APPEAL_APPROVED', 'Appeal Approved'
APPEAL_DENIED = 'APPEAL_DENIED', 'Appeal Denied'
EXPIRED = 'EXPIRED', 'Expired'
CANCELLED = 'CANCELLED', 'Cancelled'
class Priority(models.TextChoices):
ROUTINE = 'ROUTINE', 'Routine'
URGENT = 'URGENT', 'Urgent'
STAT = 'STAT', 'STAT'
EMERGENCY = 'EMERGENCY', 'Emergency'
class SubmissionMethod(models.TextChoices):
FAX = 'FAX', 'Fax'
PHONE = 'PHONE', 'Phone'
PORTAL = 'PORTAL', 'Insurance Portal'
EMAIL = 'EMAIL', 'Email'
MAIL = 'MAIL', 'Mail'
EDI = 'EDI', 'Electronic Data Interchange'
# Tenant
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='approval_requests',
help_text='Organization tenant'
)
# Identifiers
approval_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique approval identifier'
)
approval_number = models.CharField(
max_length=30,
unique=True,
help_text='Approval request number'
)
# Patient & Insurance
patient = models.ForeignKey(
'patients.PatientProfile',
on_delete=models.CASCADE,
related_name='approval_requests',
help_text='Patient'
)
insurance_info = models.ForeignKey(
'patients.InsuranceInfo',
on_delete=models.CASCADE,
related_name='approval_requests',
help_text='Insurance information'
)
# Polymorphic relationship to any order (optional - can create approval before order)
content_type = models.ForeignKey(
ContentType,
on_delete=models.CASCADE,
null=True,
blank=True,
help_text='Type of order (Lab, Radiology, Pharmacy, etc.)'
)
object_id = models.PositiveIntegerField(
null=True,
blank=True,
help_text='ID of the related order'
)
order = GenericForeignKey('content_type', 'object_id')
# Request Details
request_type = models.CharField(
max_length=20,
choices=RequestType.choices,
help_text='Type of approval request'
)
service_description = models.CharField(
max_length=500,
help_text='Description of service/procedure'
)
# Medical Codes
procedure_codes = models.JSONField(
default=list,
help_text='CPT/HCPCS procedure codes'
)
diagnosis_codes = models.JSONField(
default=list,
help_text='ICD-10 diagnosis codes'
)
# Clinical Information
clinical_justification = models.TextField(
help_text='Clinical justification for service'
)
medical_necessity = models.TextField(
blank=True,
null=True,
help_text='Medical necessity statement'
)
alternative_treatments_tried = models.TextField(
blank=True,
null=True,
help_text='Alternative treatments attempted'
)
# Requested Services
requested_quantity = models.PositiveIntegerField(
default=1,
help_text='Quantity requested'
)
requested_visits = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Number of visits requested'
)
requested_units = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Number of units requested'
)
service_start_date = models.DateField(
help_text='Requested service start date'
)
service_end_date = models.DateField(
blank=True,
null=True,
help_text='Requested service end date'
)
# Status & Priority
status = models.CharField(
max_length=30,
choices=ApprovalStatus.choices,
default=ApprovalStatus.DRAFT,
help_text='Current approval status'
)
priority = models.CharField(
max_length=20,
choices=Priority.choices,
default=Priority.ROUTINE,
help_text='Request priority'
)
# Submission Details
submission_method = models.CharField(
max_length=20,
choices=SubmissionMethod.choices,
blank=True,
null=True,
help_text='Method used to submit request'
)
submitted_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date and time submitted to insurance'
)
submitted_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='submitted_approvals',
help_text='User who submitted the request'
)
# Insurance Response
decision_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date insurance made decision'
)
authorization_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Insurance authorization number'
)
reference_number = models.CharField(
max_length=100,
blank=True,
null=True,
help_text='Insurance reference number'
)
# Approved Details
approved_quantity = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Approved quantity'
)
approved_visits = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Approved number of visits'
)
approved_units = models.PositiveIntegerField(
blank=True,
null=True,
help_text='Approved number of units'
)
approved_amount = models.DecimalField(
max_digits=12,
decimal_places=2,
blank=True,
null=True,
help_text='Approved dollar amount'
)
# Validity Period
effective_date = models.DateField(
blank=True,
null=True,
help_text='Authorization effective date'
)
expiration_date = models.DateField(
blank=True,
null=True,
help_text='Authorization expiration date'
)
# Denial Information
denial_reason = models.TextField(
blank=True,
null=True,
help_text='Reason for denial'
)
denial_code = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Insurance denial code'
)
# Appeal Information
appeal_date = models.DateTimeField(
blank=True,
null=True,
help_text='Date appeal was filed'
)
appeal_reason = models.TextField(
blank=True,
null=True,
help_text='Reason for appeal'
)
appeal_deadline = models.DateField(
blank=True,
null=True,
help_text='Deadline to file appeal'
)
# Assignment & Tracking
assigned_to = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='assigned_approvals',
help_text='Staff member assigned to this request'
)
requesting_provider = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='requested_approvals',
help_text='Provider requesting the approval'
)
# Communication Log
last_contact_date = models.DateTimeField(
blank=True,
null=True,
help_text='Last contact with insurance'
)
last_contact_method = models.CharField(
max_length=20,
blank=True,
null=True,
help_text='Method of last contact'
)
last_contact_notes = models.TextField(
blank=True,
null=True,
help_text='Notes from last contact'
)
# Flags
is_urgent = models.BooleanField(
default=False,
help_text='Urgent request'
)
is_expedited = models.BooleanField(
default=False,
help_text='Expedited processing requested'
)
requires_peer_review = models.BooleanField(
default=False,
help_text='Requires peer-to-peer review'
)
# Notes
internal_notes = models.TextField(
blank=True,
null=True,
help_text='Internal staff notes'
)
insurance_notes = models.TextField(
blank=True,
null=True,
help_text='Notes from insurance company'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_approvals',
help_text='User who created the request'
)
class Meta:
db_table = 'insurance_approvals_request'
verbose_name = 'Insurance Approval Request'
verbose_name_plural = 'Insurance Approval Requests'
ordering = ['-created_at']
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['patient', 'status']),
models.Index(fields=['insurance_info', 'status']),
models.Index(fields=['approval_number']),
models.Index(fields=['authorization_number']),
models.Index(fields=['expiration_date']),
models.Index(fields=['assigned_to', 'status']),
models.Index(fields=['content_type', 'object_id']),
models.Index(fields=['priority', 'status']),
]
def __str__(self):
return f"{self.approval_number} - {self.patient.get_full_name()} - {self.get_request_type_display()}"
def save(self, *args, **kwargs):
"""Generate approval number if not provided."""
if not self.approval_number:
today = timezone.now().date()
last_approval = InsuranceApprovalRequest.objects.filter(
tenant=self.tenant,
created_at__date=today
).order_by('-id').first()
if last_approval:
last_num = int(last_approval.approval_number.split('-')[-1])
self.approval_number = f"AUTH-{today.strftime('%Y%m%d')}-{last_num + 1:04d}"
else:
self.approval_number = f"AUTH-{today.strftime('%Y%m%d')}-0001"
super().save(*args, **kwargs)
@property
def is_approved(self):
"""Check if request is approved."""
return self.status in ['APPROVED', 'PARTIALLY_APPROVED', 'APPEAL_APPROVED']
@property
def is_denied(self):
"""Check if request is denied."""
return self.status in ['DENIED', 'APPEAL_DENIED']
@property
def is_expired(self):
"""Check if authorization is expired."""
if self.expiration_date:
return timezone.now().date() > self.expiration_date
return False
@property
def days_until_expiry(self):
"""Calculate days until expiration."""
if self.expiration_date:
return (self.expiration_date - timezone.now().date()).days
return None
@property
def is_expiring_soon(self):
"""Check if expiring within 30 days."""
days = self.days_until_expiry
return days is not None and 0 < days <= 30
@property
def can_appeal(self):
"""Check if request can be appealed."""
if not self.is_denied:
return False
if not self.appeal_deadline:
return True
return timezone.now().date() <= self.appeal_deadline
@property
def turnaround_time_days(self):
"""Calculate turnaround time in days."""
if self.submitted_date and self.decision_date:
return (self.decision_date - self.submitted_date).days
return None
class ApprovalDocument(models.Model):
"""Supporting documents for approval requests."""
class DocumentType(models.TextChoices):
MEDICAL_RECORDS = 'MEDICAL_RECORDS', 'Medical Records'
LAB_RESULTS = 'LAB_RESULTS', 'Lab Results'
IMAGING_REPORTS = 'IMAGING_REPORTS', 'Imaging Reports'
CLINICAL_NOTES = 'CLINICAL_NOTES', 'Clinical Notes'
PRESCRIPTION = 'PRESCRIPTION', 'Prescription'
LETTER_OF_MEDICAL_NECESSITY = 'LETTER_OF_MEDICAL_NECESSITY', 'Letter of Medical Necessity'
PRIOR_AUTH_FORM = 'PRIOR_AUTH_FORM', 'Prior Authorization Form'
INSURANCE_CARD = 'INSURANCE_CARD', 'Insurance Card'
CONSENT_FORM = 'CONSENT_FORM', 'Consent Form'
APPEAL_LETTER = 'APPEAL_LETTER', 'Appeal Letter'
PEER_REVIEW = 'PEER_REVIEW', 'Peer Review Documentation'
OTHER = 'OTHER', 'Other'
approval_request = models.ForeignKey(
InsuranceApprovalRequest,
on_delete=models.CASCADE,
related_name='documents',
help_text='Related approval request'
)
document_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique document identifier'
)
document_type = models.CharField(
max_length=30,
choices=DocumentType.choices,
help_text='Type of document'
)
title = models.CharField(
max_length=200,
help_text='Document title'
)
description = models.TextField(
blank=True,
null=True,
help_text='Document description'
)
file = models.FileField(
upload_to='approval_documents/%Y/%m/',
help_text='Document file'
)
file_size = models.PositiveIntegerField(
help_text='File size in bytes'
)
mime_type = models.CharField(
max_length=100,
help_text='MIME type'
)
uploaded_at = models.DateTimeField(auto_now_add=True)
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='uploaded_approval_documents',
help_text='User who uploaded the document'
)
class Meta:
db_table = 'insurance_approvals_document'
verbose_name = 'Approval Document'
verbose_name_plural = 'Approval Documents'
ordering = ['-uploaded_at']
indexes = [
models.Index(fields=['approval_request', 'document_type']),
]
def __str__(self):
return f"{self.title} - {self.approval_request.approval_number}"
@property
def file_size_mb(self):
"""Get file size in MB."""
return round(self.file_size / (1024 * 1024), 2)
class ApprovalStatusHistory(models.Model):
"""Audit trail for approval status changes."""
approval_request = models.ForeignKey(
InsuranceApprovalRequest,
on_delete=models.CASCADE,
related_name='status_history',
help_text='Related approval request'
)
from_status = models.CharField(
max_length=30,
blank=True,
null=True,
help_text='Previous status'
)
to_status = models.CharField(
max_length=30,
help_text='New status'
)
reason = models.TextField(
blank=True,
null=True,
help_text='Reason for status change'
)
notes = models.TextField(
blank=True,
null=True,
help_text='Additional notes'
)
changed_at = models.DateTimeField(auto_now_add=True)
changed_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approval_status_changes',
help_text='User who made the change'
)
class Meta:
db_table = 'insurance_approvals_status_history'
verbose_name = 'Approval Status History'
verbose_name_plural = 'Approval Status Histories'
ordering = ['-changed_at']
indexes = [
models.Index(fields=['approval_request', 'changed_at']),
]
def __str__(self):
return f"{self.approval_request.approval_number}: {self.from_status}{self.to_status}"
class ApprovalCommunicationLog(models.Model):
"""Log of all communications with insurance company."""
class CommunicationType(models.TextChoices):
PHONE_CALL = 'PHONE_CALL', 'Phone Call'
FAX_SENT = 'FAX_SENT', 'Fax Sent'
FAX_RECEIVED = 'FAX_RECEIVED', 'Fax Received'
EMAIL_SENT = 'EMAIL_SENT', 'Email Sent'
EMAIL_RECEIVED = 'EMAIL_RECEIVED', 'Email Received'
PORTAL_MESSAGE = 'PORTAL_MESSAGE', 'Portal Message'
MAIL_SENT = 'MAIL_SENT', 'Mail Sent'
MAIL_RECEIVED = 'MAIL_RECEIVED', 'Mail Received'
IN_PERSON = 'IN_PERSON', 'In Person'
approval_request = models.ForeignKey(
InsuranceApprovalRequest,
on_delete=models.CASCADE,
related_name='communications',
help_text='Related approval request'
)
communication_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique communication identifier'
)
communication_type = models.CharField(
max_length=20,
choices=CommunicationType.choices,
help_text='Type of communication'
)
contact_person = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Insurance contact person'
)
contact_number = models.CharField(
max_length=50,
blank=True,
null=True,
help_text='Contact phone/fax number'
)
subject = models.CharField(
max_length=200,
help_text='Communication subject'
)
message = models.TextField(
help_text='Message content'
)
response = models.TextField(
blank=True,
null=True,
help_text='Response received'
)
outcome = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Communication outcome'
)
follow_up_required = models.BooleanField(
default=False,
help_text='Follow-up required'
)
follow_up_date = models.DateField(
blank=True,
null=True,
help_text='Follow-up date'
)
communicated_at = models.DateTimeField(auto_now_add=True)
communicated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='approval_communications',
help_text='User who made the communication'
)
class Meta:
db_table = 'insurance_approvals_communication_log'
verbose_name = 'Approval Communication Log'
verbose_name_plural = 'Approval Communication Logs'
ordering = ['-communicated_at']
indexes = [
models.Index(fields=['approval_request', 'communicated_at']),
models.Index(fields=['follow_up_required', 'follow_up_date']),
]
def __str__(self):
return f"{self.approval_request.approval_number} - {self.get_communication_type_display()} - {self.communicated_at.date()}"
class ApprovalTemplate(models.Model):
"""Templates for common approval requests."""
tenant = models.ForeignKey(
'core.Tenant',
on_delete=models.CASCADE,
related_name='approval_templates',
help_text='Organization tenant'
)
template_id = models.UUIDField(
default=uuid.uuid4,
unique=True,
editable=False,
help_text='Unique template identifier'
)
name = models.CharField(
max_length=200,
help_text='Template name'
)
description = models.TextField(
blank=True,
null=True,
help_text='Template description'
)
request_type = models.CharField(
max_length=20,
help_text='Type of request this template is for'
)
insurance_company = models.CharField(
max_length=200,
blank=True,
null=True,
help_text='Specific insurance company (optional)'
)
# Template content
clinical_justification_template = models.TextField(
help_text='Template for clinical justification'
)
medical_necessity_template = models.TextField(
blank=True,
null=True,
help_text='Template for medical necessity statement'
)
required_documents = models.JSONField(
default=list,
help_text='List of required document types'
)
required_codes = models.JSONField(
default=dict,
help_text='Required procedure/diagnosis codes'
)
is_active = models.BooleanField(
default=True,
help_text='Template is active'
)
usage_count = models.PositiveIntegerField(
default=0,
help_text='Number of times template has been used'
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_approval_templates',
help_text='User who created the template'
)
class Meta:
db_table = 'insurance_approvals_template'
verbose_name = 'Approval Template'
verbose_name_plural = 'Approval Templates'
ordering = ['name']
indexes = [
models.Index(fields=['tenant', 'is_active']),
models.Index(fields=['request_type']),
]
unique_together = ['tenant', 'name']
def __str__(self):
return f"{self.name} ({self.request_type})"