""" Billing app models for hospital management system. Provides medical billing, insurance claims, and revenue cycle management. """ import uuid from django.db import models from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator from django.utils import timezone from django.conf import settings from datetime import timedelta, datetime, date from decimal import Decimal import json from .utils import default_aging_buckets class MedicalBill(models.Model): """ Medical bill model for patient billing and revenue cycle management. """ class BillType(models.TextChoices): INPATIENT = 'INPATIENT', 'Inpatient' OUTPATIENT = 'OUTPATIENT', 'Outpatient' EMERGENCY = 'EMERGENCY', 'Emergency' SURGERY = 'SURGERY', 'Surgery' LABORATORY = 'LABORATORY', 'Laboratory' RADIOLOGY = 'RADIOLOGY', 'Radiology' PHARMACY = 'PHARMACY', 'Pharmacy' PROFESSIONAL = 'PROFESSIONAL', 'Professional Services' FACILITY = 'FACILITY', 'Facility Charges' ANCILLARY = 'ANCILLARY', 'Ancillary Services' class BillStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' PENDING = 'PENDING', 'Pending' SUBMITTED = 'SUBMITTED', 'Submitted' PARTIAL_PAID = 'PARTIAL_PAID', 'Partially Paid' # kept as provided PAID = 'PAID', 'Paid' OVERDUE = 'OVERDUE', 'Overdue' COLLECTIONS = 'COLLECTIONS', 'Collections' WRITTEN_OFF = 'WRITTEN_OFF', 'Written Off' CANCELLED = 'CANCELLED', 'Cancelled' class PaymentTerms(models.TextChoices): NET_30 = 'NET_30', 'Net 30 Days' NET_60 = 'NET_60', 'Net 60 Days' NET_90 = 'NET_90', 'Net 90 Days' IMMEDIATE = 'IMMEDIATE', 'Immediate' CUSTOM = 'CUSTOM', 'Custom Terms' class CollectionStatus(models.TextChoices): NONE = 'NONE', 'None' FIRST_NOTICE = 'FIRST_NOTICE', 'First Notice' SECOND_NOTICE = 'SECOND_NOTICE', 'Second Notice' FINAL_NOTICE = 'FINAL_NOTICE', 'Final Notice' COLLECTIONS = 'COLLECTIONS', 'Collections' LEGAL = 'LEGAL', 'Legal Action' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='medical_bills', help_text='Organization tenant' ) # Bill Information bill_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique bill identifier' ) bill_number = models.CharField( max_length=20, unique=True, help_text='Medical bill number' ) # Patient Information patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='medical_bills', help_text='Patient' ) # Bill Type and Category bill_type = models.CharField( max_length=20, choices=BillType.choices, help_text='Bill type' ) # Bill Dates service_date_from = models.DateField( help_text='Service date from' ) service_date_to = models.DateField( help_text='Service date to' ) bill_date = models.DateField( default=timezone.now, help_text='Bill date' ) due_date = models.DateField( help_text='Payment due date' ) # Financial Information subtotal = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Subtotal amount' ) tax_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Tax amount' ) discount_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Discount amount' ) adjustment_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Adjustment amount' ) total_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Total bill amount' ) paid_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Amount paid' ) balance_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Balance amount' ) # Insurance Information primary_insurance = models.ForeignKey( 'patients.InsuranceInfo', on_delete=models.SET_NULL, null=True, blank=True, related_name='primary_bills', help_text='Primary insurance' ) secondary_insurance = models.ForeignKey( 'patients.InsuranceInfo', on_delete=models.SET_NULL, null=True, blank=True, related_name='secondary_bills', help_text='Secondary insurance' ) # Bill Status status = models.CharField( max_length=20, choices=BillStatus.choices, default=BillStatus.DRAFT, help_text='Bill status' ) # Provider Information attending_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='attending_bills', help_text='Attending provider' ) billing_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='billing_provider_bills', help_text='Billing provider' ) # Related Information encounter = models.ForeignKey( 'emr.Encounter', on_delete=models.SET_NULL, null=True, blank=True, related_name='medical_bills', help_text='Related encounter' ) admission = models.ForeignKey( 'inpatients.Admission', on_delete=models.SET_NULL, null=True, blank=True, related_name='medical_bills', help_text='Related admission' ) # Billing Notes notes = models.TextField( blank=True, null=True, help_text='Billing notes and comments' ) # Payment Terms payment_terms = models.CharField( max_length=20, choices=PaymentTerms.choices, default=PaymentTerms.NET_30, help_text='Payment terms' ) # Collection Information collection_status = models.CharField( max_length=20, choices=CollectionStatus.choices, default=CollectionStatus.NONE, help_text='Collection status' ) last_statement_date = models.DateField( blank=True, null=True, help_text='Last statement date' ) # 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_medical_bills', help_text='User who created the bill' ) class Meta: db_table = 'billing_medical_bill' verbose_name = 'Medical Bill' verbose_name_plural = 'Medical Bills' ordering = ['-bill_date'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'bill_date']), models.Index(fields=['bill_number']), models.Index(fields=['status', 'due_date']), models.Index(fields=['collection_status']), ] def __str__(self): return f"{self.bill_number} - {self.patient.get_full_name()}" def save(self, *args, **kwargs): """ Generate bill number and calculate totals. """ if not self.bill_number: # Generate bill number (simple implementation) today = timezone.now().date() last_bill = MedicalBill.objects.filter( tenant=self.tenant, created_at__date=today ).order_by('-id').first() if last_bill: last_number = int(last_bill.bill_number.split('-')[-1]) self.bill_number = f"BILL-{today.strftime('%Y%m%d')}-{last_number + 1:04d}" else: self.bill_number = f"BILL-{today.strftime('%Y%m%d')}-0001" # Calculate totals self.total_amount = self.subtotal + self.tax_amount - self.discount_amount + self.adjustment_amount self.balance_amount = self.total_amount - self.paid_amount super().save(*args, **kwargs) @property def is_overdue(self): """ Check if bill is overdue. """ return self.due_date < timezone.now().date() and self.balance_amount > 0 @property def days_outstanding(self): """ Calculate days outstanding. """ return (timezone.now().date() - self.bill_date).days @property def payment_percentage(self): """ Calculate payment percentage. """ if self.total_amount > 0: return round((self.paid_amount / self.total_amount) * 100, 1) return 0 class BillLineItem(models.Model): """ Bill line item model for detailed billing charges. """ class ServiceCategory(models.TextChoices): EVALUATION = 'EVALUATION', 'Evaluation & Management' SURGERY = 'SURGERY', 'Surgery' RADIOLOGY = 'RADIOLOGY', 'Radiology' PATHOLOGY = 'PATHOLOGY', 'Pathology & Laboratory' MEDICINE = 'MEDICINE', 'Medicine' ANESTHESIA = 'ANESTHESIA', 'Anesthesia' SUPPLIES = 'SUPPLIES', 'Medical Supplies' PHARMACY = 'PHARMACY', 'Pharmacy' ROOM_BOARD = 'ROOM_BOARD', 'Room & Board' NURSING = 'NURSING', 'Nursing Services' THERAPY = 'THERAPY', 'Therapy Services' EMERGENCY = 'EMERGENCY', 'Emergency Services' AMBULANCE = 'AMBULANCE', 'Ambulance Services' DME = 'DME', 'Durable Medical Equipment' OTHER = 'OTHER', 'Other Services' class ServiceUnitOfMeasure(models.TextChoices): EACH = 'EACH', 'Each' UNIT = 'UNIT', 'Unit' HOUR = 'HOUR', 'Hour' DAY = 'DAY', 'Day' VISIT = 'VISIT', 'Visit' PROCEDURE = 'PROCEDURE', 'Procedure' DOSE = 'DOSE', 'Dose' MILE = 'MILE', 'Mile' MINUTE = 'MINUTE', 'Minute' class PlaceOfService(models.IntegerChoices): OFFICE = 11, 'Office' HOME = 12, 'Home' INPATIENT_HOSPITAL = 21, 'Inpatient Hospital' OUTPATIENT_HOSPITAL = 22, 'Outpatient Hospital' EMERGENCY_ROOM = 23, 'Emergency Room' AMBULATORY_SURGICAL_CENTER = 24, 'Ambulatory Surgical Center' BIRTHING_CENTER = 25, 'Birthing Center' MILITARY_TREATMENT_FACILITY = 26, 'Military Treatment Facility' SKILLED_NURSING_FACILITY = 31, 'Skilled Nursing Facility' NURSING_FACILITY = 32, 'Nursing Facility' CUSTODIAL_CARE_FACILITY = 33, 'Custodial Care Facility' HOSPICE = 34, 'Hospice' AMBULANCE_LAND = 41, 'Ambulance - Land' AMBULANCE_AIR_OR_WATER = 42, 'Ambulance - Air or Water' INDEPENDENT_CLINIC = 49, 'Independent Clinic' FEDERALLY_QUALIFIED_HEALTH_CENTER = 50, 'Federally Qualified Health Center' INPATIENT_PSYCHIATRIC_FACILITY = 51, 'Inpatient Psychiatric Facility' PSYCHIATRIC_PARTIAL_HOSPITALIZATION = 52, 'Psychiatric Facility-Partial Hospitalization' COMMUNITY_MENTAL_HEALTH_CENTER = 53, 'Community Mental Health Center' INTERMEDIATE_CARE_FACILITY_MR = 54, 'Intermediate Care Facility/Mentally Retarded' RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 55, 'Residential Substance Abuse Treatment Facility' PSYCHIATRIC_RESIDENTIAL_TREATMENT_CENTER = 56, 'Psychiatric Residential Treatment Center' NON_RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 57, 'Non-residential Substance Abuse Treatment Facility' MASS_IMMUNIZATION_CENTER = 60, 'Mass Immunization Center' COMPREHENSIVE_INPATIENT_REHAB_FACILITY = 61, 'Comprehensive Inpatient Rehabilitation Facility' COMPREHENSIVE_OUTPATIENT_REHAB_FACILITY = 62, 'Comprehensive Outpatient Rehabilitation Facility' ESRD_TREATMENT_FACILITY = 65, 'End-Stage Renal Disease Treatment Facility' PUBLIC_HEALTH_CLINIC = 71, 'Public Health Clinic' RURAL_HEALTH_CLINIC = 72, 'Rural Health Clinic' INDEPENDENT_LABORATORY = 81, 'Independent Laboratory' OTHER_PLACE_OF_SERVICE = 99, 'Other Place of Service' class ServiceStatus(models.TextChoices): ACTIVE = 'ACTIVE', 'Active' DENIED = 'DENIED', 'Denied' ADJUSTED = 'ADJUSTED', 'Adjusted' VOIDED = 'VOIDED', 'Voided' # Medical Bill relationship medical_bill = models.ForeignKey( MedicalBill, on_delete=models.CASCADE, related_name='line_items', help_text='Medical bill' ) # Line Item Information line_item_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique line item identifier' ) line_number = models.PositiveIntegerField( help_text='Line item number' ) # Service Information service_date = models.DateField( help_text='Service date' ) service_code = models.CharField( max_length=20, help_text='Service code (CPT, HCPCS, etc.)' ) service_description = models.CharField( max_length=200, help_text='Service description' ) # Service Category service_category = models.CharField( max_length=30, choices=ServiceCategory.choices, help_text='Service category' ) # Quantity and Units quantity = models.DecimalField( max_digits=10, decimal_places=2, default=Decimal('1.00'), help_text='Quantity of service' ) unit_of_measure = models.CharField( max_length=20, choices=ServiceUnitOfMeasure.choices, default=ServiceUnitOfMeasure.EACH, help_text='Unit of measure' ) # Pricing Information unit_price = models.DecimalField( max_digits=10, decimal_places=2, help_text='Unit price' ) total_price = models.DecimalField( max_digits=12, decimal_places=2, help_text='Total price (quantity × unit price)' ) # Modifiers modifier_1 = models.CharField( max_length=5, blank=True, null=True, help_text='First modifier' ) modifier_2 = models.CharField( max_length=5, blank=True, null=True, help_text='Second modifier' ) modifier_3 = models.CharField( max_length=5, blank=True, null=True, help_text='Third modifier' ) modifier_4 = models.CharField( max_length=5, blank=True, null=True, help_text='Fourth modifier' ) # Diagnosis Information primary_diagnosis = models.CharField( max_length=20, blank=True, null=True, help_text='Primary diagnosis code' ) secondary_diagnoses = models.JSONField( default=list, help_text='Secondary diagnosis codes' ) # Provider Information rendering_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='rendered_line_items', help_text='Rendering provider' ) supervising_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='supervised_line_items', help_text='Supervising provider' ) # Place of Service place_of_service = models.IntegerField( choices=PlaceOfService.choices, default=22, help_text='Place of service code' ) # Revenue Code (for facility billing) revenue_code = models.CharField( max_length=4, blank=True, null=True, help_text='Revenue code for facility billing' ) # NDC Information (for drugs) ndc_code = models.CharField( max_length=20, blank=True, null=True, help_text='National Drug Code' ) drug_quantity = models.DecimalField( max_digits=10, decimal_places=3, blank=True, null=True, help_text='Drug quantity' ) drug_unit = models.CharField( max_length=10, blank=True, null=True, help_text='Drug unit of measure' ) # Line Item Status status = models.CharField( max_length=20, choices=ServiceStatus.choices, default=ServiceStatus.ACTIVE, help_text='Line item status' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Line item notes' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'billing_bill_line_item' verbose_name = 'Bill Line Item' verbose_name_plural = 'Bill Line Items' ordering = ['line_number'] indexes = [ models.Index(fields=['medical_bill', 'line_number']), models.Index(fields=['service_code']), models.Index(fields=['service_date']), models.Index(fields=['rendering_provider']), ] unique_together = ['medical_bill', 'line_number'] def __str__(self): return f"{self.medical_bill.bill_number} - Line {self.line_number}" def save(self, *args, **kwargs): """ Calculate total price from quantity and unit price. """ self.total_price = self.quantity * self.unit_price super().save(*args, **kwargs) @property def patient(self): """ Get patient from medical bill. """ return self.medical_bill.patient @property def tenant(self): """ Get tenant from medical bill. """ return self.medical_bill.tenant class InsuranceClaim(models.Model): """ Insurance claim model for claims processing and management. """ class ClaimSubmissionType(models.TextChoices): PRIMARY = 'PRIMARY', 'Primary Claim' SECONDARY = 'SECONDARY', 'Secondary Claim' TERTIARY = 'TERTIARY', 'Tertiary Claim' CORRECTED = 'CORRECTED', 'Corrected Claim' VOID = 'VOID', 'Void Claim' REPLACEMENT = 'REPLACEMENT', 'Replacement Claim' class ClaimProcessingStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' SUBMITTED = 'SUBMITTED', 'Submitted' PENDING = 'PENDING', 'Pending' PROCESSING = 'PROCESSING', 'Processing' PAID = 'PAID', 'Paid' DENIED = 'DENIED', 'Denied' REJECTED = 'REJECTED', 'Rejected' APPEALED = 'APPEALED', 'Appealed' VOIDED = 'VOIDED', 'Voided' # Medical Bill relationship medical_bill = models.ForeignKey( MedicalBill, on_delete=models.CASCADE, related_name='insurance_claims', help_text='Related medical bill' ) # Claim Information claim_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique claim identifier' ) claim_number = models.CharField( max_length=30, unique=True, help_text='Insurance claim number' ) # Insurance Information insurance_info = models.ForeignKey( 'patients.InsuranceInfo', on_delete=models.CASCADE, related_name='insurance_claims', help_text='Insurance information' ) # Claim Type claim_type = models.CharField( max_length=20, choices=ClaimSubmissionType.choices, default=ClaimSubmissionType.PRIMARY, help_text='Claim type' ) # Claim Dates submission_date = models.DateField( help_text='Claim submission date' ) service_date_from = models.DateField( help_text='Service date from' ) service_date_to = models.DateField( help_text='Service date to' ) # Financial Information billed_amount = models.DecimalField( max_digits=12, decimal_places=2, help_text='Total billed amount' ) allowed_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Insurance allowed amount' ) paid_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Insurance paid amount' ) patient_responsibility = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Patient responsibility amount' ) deductible_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Deductible amount' ) coinsurance_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Coinsurance amount' ) copay_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Copay amount' ) # Claim Status status = models.CharField( max_length=20, choices=ClaimProcessingStatus.choices, default=ClaimProcessingStatus.DRAFT, help_text='Claim status' ) # Processing Information clearinghouse = models.CharField( max_length=100, blank=True, null=True, help_text='Clearinghouse used for submission' ) batch_number = models.CharField( max_length=50, blank=True, null=True, help_text='Batch number' ) # Response Information response_date = models.DateField( blank=True, null=True, help_text='Insurance response date' ) check_number = models.CharField( max_length=50, blank=True, null=True, help_text='Insurance check number' ) check_date = models.DateField( blank=True, null=True, help_text='Insurance check date' ) # Denial Information denial_reason = models.CharField( max_length=200, blank=True, null=True, help_text='Denial reason' ) denial_code = models.CharField( max_length=20, blank=True, null=True, help_text='Denial code' ) # Prior Authorization prior_auth_number = models.CharField( max_length=50, blank=True, null=True, help_text='Prior authorization number' ) # Claim Notes notes = models.TextField( blank=True, null=True, help_text='Claim notes and comments' ) # Resubmission Information original_claim = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='resubmissions', help_text='Original claim if this is a resubmission' ) resubmission_count = models.PositiveIntegerField( default=0, help_text='Number of resubmissions' ) # 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_insurance_claims', help_text='User who created the claim' ) class Meta: db_table = 'billing_insurance_claim' verbose_name = 'Insurance Claim' verbose_name_plural = 'Insurance Claims' ordering = ['-submission_date'] indexes = [ models.Index(fields=['medical_bill']), models.Index(fields=['insurance_info']), models.Index(fields=['claim_number']), models.Index(fields=['status', 'submission_date']), models.Index(fields=['response_date']), ] def __str__(self): return f"{self.claim_number} - {self.insurance_info.insurance_company}" def save(self, *args, **kwargs): """ Generate claim number if not provided. """ if not self.claim_number: # Generate claim number (simple implementation) today = timezone.now().date() last_claim = InsuranceClaim.objects.filter( medical_bill__tenant=self.medical_bill.tenant, created_at__date=today ).order_by('-id').first() if last_claim: last_number = int(last_claim.claim_number.split('-')[-1]) self.claim_number = f"CLM-{today.strftime('%Y%m%d')}-{last_number + 1:04d}" else: self.claim_number = f"CLM-{today.strftime('%Y%m%d')}-0001" super().save(*args, **kwargs) @property def patient(self): """ Get patient from medical bill. """ return self.medical_bill.patient @property def tenant(self): """ Get tenant from medical bill. """ return self.medical_bill.tenant @property def days_pending(self): """ Calculate days pending since submission. """ return (timezone.now().date() - self.submission_date).days @property def payment_percentage(self): """ Calculate payment percentage. """ if self.billed_amount > 0: return round((self.paid_amount / self.billed_amount) * 100, 1) return 0 class Payment(models.Model): """ Payment model for tracking payments and receipts. """ class PaymentMethod(models.TextChoices): CASH = 'CASH', 'Cash' CHECK = 'CHECK', 'Check' CREDIT_CARD = 'CREDIT_CARD', 'Credit Card' DEBIT_CARD = 'DEBIT_CARD', 'Debit Card' BANK_TRANSFER = 'BANK_TRANSFER', 'Bank Transfer' ACH = 'ACH', 'ACH Transfer' WIRE = 'WIRE', 'Wire Transfer' MONEY_ORDER = 'MONEY_ORDER', 'Money Order' INSURANCE = 'INSURANCE', 'Insurance Payment' ADJUSTMENT = 'ADJUSTMENT', 'Adjustment' WRITE_OFF = 'WRITE_OFF', 'Write Off' OTHER = 'OTHER', 'Other' class PaymentSource(models.TextChoices): PATIENT = 'PATIENT', 'Patient' INSURANCE = 'INSURANCE', 'Insurance' GUARANTOR = 'GUARANTOR', 'Guarantor' GOVERNMENT = 'GOVERNMENT', 'Government' CHARITY = 'CHARITY', 'Charity' OTHER = 'OTHER', 'Other' class PaymentStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' PROCESSED = 'PROCESSED', 'Processed' CLEARED = 'CLEARED', 'Cleared' BOUNCED = 'BOUNCED', 'Bounced' REVERSED = 'REVERSED', 'Reversed' REFUNDED = 'REFUNDED', 'Refunded' class CardType(models.TextChoices): VISA = 'VISA', 'Visa' MASTERCARD = 'MASTERCARD', 'MasterCard' AMEX = 'AMEX', 'American Express' DISCOVER = 'DISCOVER', 'Discover' OTHER = 'OTHER', 'Other' # Medical Bill relationship medical_bill = models.ForeignKey( MedicalBill, on_delete=models.CASCADE, related_name='payments', help_text='Related medical bill' ) # Payment Information payment_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique payment identifier' ) payment_number = models.CharField( max_length=20, unique=True, help_text='Payment number' ) # Payment Details payment_date = models.DateField( help_text='Payment date' ) payment_amount = models.DecimalField( max_digits=12, decimal_places=2, help_text='Payment amount' ) # Payment Method payment_method = models.CharField( max_length=20, choices=PaymentMethod.choices, help_text='Payment method' ) # Payment Source payment_source = models.CharField( max_length=20, choices=PaymentSource.choices, help_text='Payment source' ) # Check Information check_number = models.CharField( max_length=50, blank=True, null=True, help_text='Check number' ) bank_name = models.CharField( max_length=100, blank=True, null=True, help_text='Bank name' ) routing_number = models.CharField( max_length=20, blank=True, null=True, help_text='Bank routing number' ) # Credit Card Information (encrypted/tokenized) card_type = models.CharField( max_length=20, choices=CardType.choices, blank=True, null=True, help_text='Credit card type' ) card_last_four = models.CharField( max_length=4, blank=True, null=True, help_text='Last four digits of card' ) authorization_code = models.CharField( max_length=20, blank=True, null=True, help_text='Authorization code' ) transaction_id = models.CharField( max_length=50, blank=True, null=True, help_text='Transaction ID' ) # Insurance Payment Information insurance_claim = models.ForeignKey( InsuranceClaim, on_delete=models.SET_NULL, null=True, blank=True, related_name='payments', help_text='Related insurance claim' ) eob_number = models.CharField( max_length=50, blank=True, null=True, help_text='Explanation of Benefits number' ) # Payment Status status = models.CharField( max_length=20, choices=PaymentStatus.choices, default=PaymentStatus.PENDING, help_text='Payment status' ) # Deposit Information deposit_date = models.DateField( blank=True, null=True, help_text='Deposit date' ) deposit_slip = models.CharField( max_length=50, blank=True, null=True, help_text='Deposit slip number' ) # Payment Notes notes = models.TextField( blank=True, null=True, help_text='Payment notes and comments' ) # Refund Information refund_amount = models.DecimalField( max_digits=12, decimal_places=2, default=Decimal('0.00'), help_text='Refund amount' ) refund_date = models.DateField( blank=True, null=True, help_text='Refund date' ) refund_reason = models.CharField( max_length=200, blank=True, null=True, help_text='Refund reason' ) # Staff Information received_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='received_payments', help_text='Staff member who received payment' ) processed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='processed_payments', help_text='Staff member who processed payment' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'billing_payment' verbose_name = 'Payment' verbose_name_plural = 'Payments' ordering = ['-payment_date'] indexes = [ models.Index(fields=['medical_bill']), models.Index(fields=['payment_number']), models.Index(fields=['payment_date']), models.Index(fields=['payment_method']), models.Index(fields=['status']), ] def __str__(self): return f"{self.payment_number} - ${self.payment_amount}" def save(self, *args, **kwargs): """ Generate payment number if not provided. """ if not self.payment_number: # Generate payment number (simple implementation) today = timezone.now().date() last_payment = Payment.objects.filter( medical_bill__tenant=self.medical_bill.tenant, created_at__date=today ).order_by('-id').first() if last_payment: last_number = int(last_payment.payment_number.split('-')[-1]) self.payment_number = f"PAY-{today.strftime('%Y%m%d')}-{last_number + 1:04d}" else: self.payment_number = f"PAY-{today.strftime('%Y%m%d')}-0001" super().save(*args, **kwargs) @property def patient(self): """ Get patient from medical bill. """ return self.medical_bill.patient @property def tenant(self): """ Get tenant from medical bill. """ return self.medical_bill.tenant @property def net_payment(self): """ Calculate net payment after refunds. """ return self.payment_amount - self.refund_amount class ClaimStatusUpdate(models.Model): """ Claim status update model for tracking claim processing history. """ class UpdateSource(models.TextChoices): MANUAL = 'MANUAL', 'Manual Update' EDI = 'EDI', 'EDI Response' PHONE = 'PHONE', 'Phone Call' PORTAL = 'PORTAL', 'Insurance Portal' EMAIL = 'EMAIL', 'Email' FAX = 'FAX', 'Fax' MAIL = 'MAIL', 'Mail' SYSTEM = 'SYSTEM', 'System Generated' # Insurance Claim relationship insurance_claim = models.ForeignKey( InsuranceClaim, on_delete=models.CASCADE, related_name='status_updates', help_text='Related insurance claim' ) # Update Information update_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique update identifier' ) # Status Information previous_status = models.CharField( max_length=20, help_text='Previous claim status' ) new_status = models.CharField( max_length=20, help_text='New claim status' ) status_date = models.DateTimeField( default=timezone.now, help_text='Status change date and time' ) # Update Details update_source = models.CharField( max_length=20, choices=UpdateSource.choices, help_text='Update source' ) # Response Information response_code = models.CharField( max_length=20, blank=True, null=True, help_text='Insurance response code' ) response_message = models.TextField( blank=True, null=True, help_text='Insurance response message' ) # Financial Updates allowed_amount = models.DecimalField( max_digits=12, decimal_places=2, blank=True, null=True, help_text='Updated allowed amount' ) paid_amount = models.DecimalField( max_digits=12, decimal_places=2, blank=True, null=True, help_text='Updated paid amount' ) patient_responsibility = models.DecimalField( max_digits=12, decimal_places=2, blank=True, null=True, help_text='Updated patient responsibility' ) # Update Notes notes = models.TextField( blank=True, null=True, help_text='Update notes and comments' ) # Staff Information updated_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='claim_status_updates', help_text='Staff member who made the update' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'billing_claim_status_update' verbose_name = 'Claim Status Update' verbose_name_plural = 'Claim Status Updates' ordering = ['-status_date'] indexes = [ models.Index(fields=['insurance_claim']), models.Index(fields=['status_date']), models.Index(fields=['new_status']), ] def __str__(self): return f"{self.insurance_claim.claim_number} - {self.previous_status} → {self.new_status}" @property def patient(self): """ Get patient from insurance claim. """ return self.insurance_claim.patient @property def tenant(self): """ Get tenant from insurance claim. """ return self.insurance_claim.tenant class BillingConfiguration(models.Model): """ Billing configuration model for tenant-specific billing settings. """ class DefaultPaymentTerms(models.TextChoices): NET_30 = 'NET_30', 'Net 30 Days' NET_60 = 'NET_60', 'Net 60 Days' NET_90 = 'NET_90', 'Net 90 Days' IMMEDIATE = 'IMMEDIATE', 'Immediate' class StatementFrequency(models.TextChoices): MONTHLY = 'MONTHLY', 'Monthly' QUARTERLY = 'QUARTERLY', 'Quarterly' ON_DEMAND = 'ON_DEMAND', 'On Demand' class ClaimSubmissionFrequency(models.TextChoices): DAILY = 'DAILY', 'Daily' WEEKLY = 'WEEKLY', 'Weekly' MANUAL = 'MANUAL', 'Manual' # Tenant relationship tenant = models.OneToOneField( 'core.Tenant', on_delete=models.CASCADE, related_name='billing_configuration', help_text='Organization tenant' ) # Configuration Information config_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique configuration identifier' ) # Billing Settings default_payment_terms = models.CharField( max_length=20, choices=DefaultPaymentTerms.choices, default=DefaultPaymentTerms.NET_30, help_text='Default payment terms' ) # Tax Settings tax_rate = models.DecimalField( max_digits=5, decimal_places=4, default=Decimal('0.0000'), help_text='Default tax rate (as decimal, e.g., 0.0825 for 8.25%)' ) tax_exempt = models.BooleanField( default=True, help_text='Organization is tax exempt' ) # Statement Settings statement_frequency = models.CharField( max_length=20, choices=StatementFrequency.choices, default=StatementFrequency.MONTHLY, help_text='Statement frequency' ) statement_message = models.TextField( blank=True, null=True, help_text='Default statement message' ) # Collection Settings first_notice_days = models.PositiveIntegerField( default=30, help_text='Days after due date for first notice' ) second_notice_days = models.PositiveIntegerField( default=60, help_text='Days after due date for second notice' ) final_notice_days = models.PositiveIntegerField( default=90, help_text='Days after due date for final notice' ) collections_days = models.PositiveIntegerField( default=120, help_text='Days after due date to send to collections' ) # Interest Settings apply_interest = models.BooleanField( default=False, help_text='Apply interest to overdue accounts' ) interest_rate = models.DecimalField( max_digits=5, decimal_places=4, default=Decimal('0.0000'), help_text='Monthly interest rate (as decimal)' ) # Payment Settings accept_credit_cards = models.BooleanField( default=True, help_text='Accept credit card payments' ) accept_ach = models.BooleanField( default=True, help_text='Accept ACH payments' ) payment_portal_enabled = models.BooleanField( default=True, help_text='Enable online payment portal' ) # Claim Settings auto_submit_claims = models.BooleanField( default=False, help_text='Automatically submit claims' ) claim_submission_frequency = models.CharField( max_length=20, choices=ClaimSubmissionFrequency.choices, default=ClaimSubmissionFrequency.DAILY, help_text='Claim submission frequency' ) # Clearinghouse Settings primary_clearinghouse = models.CharField( max_length=100, blank=True, null=True, help_text='Primary clearinghouse' ) secondary_clearinghouse = models.CharField( max_length=100, blank=True, null=True, help_text='Secondary clearinghouse' ) # Reporting Settings aging_buckets = models.JSONField( default=default_aging_buckets, help_text='Aging report buckets in days' ) # 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_billing_configurations', help_text='User who created the configuration' ) class Meta: db_table = 'billing_configuration' verbose_name = 'Billing Configuration' verbose_name_plural = 'Billing Configurations' def __str__(self): return f"Billing Config - {self.tenant.name}"