ServicesCrelio AppArchitectureApp Modules

finance

Billing, invoicing, insurance claims, and financial transactions

Finance App Architecture

Domain Responsibility

What This App Owns

  • Billing - Bill creation, updates, cancellation
  • Insurance claims - Claim submission, response processing
  • Invoices - Organization invoicing
  • Transactions - Payment tracking
  • Bill approval workflow - Discount/waiver approvals
  • Tax categories - Tax calculations

What It Depends On

  • core/ - BaseModel, utilities
  • admin/ - Labs, organizations, tests
  • patient/ - Patient details for billing

What Should NOT Be Added Here

  • Payment gateway integration (belongs in payments/)
  • Patient demographics (belongs in patient/)
  • Report generation (belongs in report/)

Model-Centric Design (Fat Models)

Key Models

ModelResponsibilityKey FieldsCross-App Relations
BillingCore bill entitylabBillId, totalAmount, pendingAmount, isCancelpatient.UserDetails, admin.Organization
InsuranceClaimInsurance claim lifecycleclaim_status, claim_id, payer_idBilling
ClaimResponseERA/EOB processingclaim_response_data, x12_835_dataInsuranceClaim
BillApprovalActionDiscount/waiver workflowaction_type, status, approved_byBilling
InsuranceGroupPayer configurationpayer_name, default_ratesadmin.Labs
InvoiceB2B invoicinginvoice_number, total, organizationadmin.Organization

Business Logic in Models

Billing (finance/models/billing.py) - 1119 lines

Key Methods:

MethodPurpose
after_save()Post-save processing
prepare_and_save_patient_insurances()Link insurance to bill
get_bills(lab_id, from_date, to_date, ...)Fetch bills with filters
get_bill_details(bill_id, lab_id, ...)Detailed bill fetch
pending_amountCalculate outstanding
add_attachment(file_details)Store bill attachments
included_testsProperty - tests in bill
get_pending_claims_qs(lab_id, from_date, to_date)Claims query

InsuranceClaim (finance/models/claim.py)

MethodPurpose
create_claim()Build claim structure
submit_claim()Send to clearinghouse
process_response()Handle ERA/EOB

BillApprovalAction (finance/models/bill_approval_action.py)

MethodPurpose
request_approval()Initiate approval workflow
approve()Grant approval
reject()Deny request

Why Fat Model Style Here

Billing logic is complex with many validation rules and state transitions. Keeping logic in models ensures:

  1. Consistent calculations (amounts, taxes, discounts)
  2. Audit trail through activity logs
  3. Integration point for claims processing

Invariants & Data Rules

Bill Constraints

ConstraintEnforcementLocation
Unique labBillId per labDatabase + generationBill creation
totalAmount = sum of test amountsCalculatedBefore save
pendingAmount = totalAmount - paymentsComputed propertypending_amount
Can't cancel after claim submissionBusiness ruleValidation

State Machine: Bill Status

FieldValuesMeaning
isCancel0/1Bill cancelled
isWriteOff0/1Written off
isComplete0/1All reports done

Claim State Machine

StatusMeaning
DRAFTNot submitted
SUBMITTEDSent to clearinghouse
ACCEPTEDPayer acknowledged
PAIDPayment received
DENIEDClaim rejected
PARTIALPartial payment

Data Access Patterns

QuerySet Optimization

# finance/models/billing.py
@classmethod
def get_bills(cls, lab_id, from_date, to_date, ...):
    return cls.objects.filter(
        labId_id=lab_id,
        registrationDate__range=(from_date, to_date),
        isCancel=0
    ).select_related(
        "userDetailsId",
        "orgId", 
        "branch"
    ).prefetch_related(
        "billinginfo_set",
        "billingicd_set"
    ).order_by("-registrationDate")

Common Query Patterns

QueryOptimization
Bill list with patientselect_related("userDetailsId")
Bill with testsprefetch_related("billinginfo_set")
Bill with ICD codesprefetch_related("billingicd_set")
Claims with responsesprefetch_related("claimresponse_set")

Database Indexes

# finance/models/billing.py
class Meta:
    indexes = [
        models.Index(fields=["labId_id", "labBillId"]),
        models.Index(fields=["orgId_id", "registrationDate"]),
        models.Index(fields=["labId_id", "isCancel", "isWriteOff", "isComplete"]),
        models.Index(fields=["labId_id", "isCancel", "isWriteOff", "lastUpdatedTime"]),
    ]

API Layer (Wiring)

Endpoint Map

EndpointViewModel Method
GET /finance/billsBillListViewBilling.get_bills()
GET /finance/bill/{id}BillDetailViewBilling.get_bill_details()
POST /finance/bill/{id}/updateBillUpdateViewbilling.save()
POST /finance/claim/submitClaimSubmitViewInsuranceClaim.submit_claim()
POST /finance/claim/responseClaimResponseViewClaimResponse.process()

Transaction Boundaries

# finance/views/bill_update.py
class BillUpdateView(GenericView):
    
    @transaction.atomic
    def post(self, request, bill_id, *args, **kwargs):
        bill = Billing.objects.get(pk=bill_id, labId_id=lab_id)
        bill.set_values(payload)
        bill.save(session=request.session)
        
        # Related updates in same transaction
        BillingInfo.objects.filter(billId=bill).update(...)
        
        return JsonResponse({"status": "success"})

Integrations

External Systems

IntegrationDirectionHandler Location
QuickBooksBidirectionalintegration/quickbooks/
Clearinghouse (X12)Outboundfinance/models/claim.py
TebraOutboundintegration/tebra/
ERA Processing (835)Inboundintegration/utils.py

X12 835 Processing

# integration/utils.py
def parse_x12_and_build_payload(claim_response, lab_id, bill_id=None):
    """Parse X12 835 ERA and build QuickBooks payload"""
    raw_text = claim_response.x12_835_data
    segments = parse_x12(raw_text)
    parsed_data = parse_835(segments)
    return build_claim_payload(parsed_data, claim_mapping, lab_id)

Side Effects & Hidden Coupling

after_save() Side Effects

# finance/models/billing.py
def after_save(self, *args, **kwargs):
    self.prepare_and_save_patient_insurances()
    # Activity logging via ActivityLogBase

Cross-App Imports

ImportUsed For
patient.models.user_details.UserDetailsLinked patient
admin.organization.models.organization.OrganizationB2B billing
admin.masters.models.all_tests.AllTestsTest pricing
report.models.lab_report_relation.LabReportRelationReport status

Performance & Scaling Notes

Hot Tables

TableVolumeIndexes
billingVery highComposite indexes
billingInfoHigh (many per bill)billId
insuranceClaimMediumbilling_id, claim_status

Query Optimization Tips

  1. Always filter by labId_id first
  2. Use date range filters
  3. Avoid full table scans with proper indexes
  4. Use values() for list views

Background Jobs

  • Claim submission via Fusion worker
  • ERA file processing
  • Invoice generation

Safe Extension Guide

Adding New Bill Field

  1. Add field to Billing model
  2. Create migration
  3. Update get_bills() return if needed
  4. Add to serializer

Adding New Financial Workflow

  1. Create new model if needed
  2. Add methods to Billing or create proxy
  3. Add view with @transaction.atomic
  4. Log activity via add_activity_log()

Patterns to Follow

  • Use @transaction.atomic for multi-model updates
  • Validate amounts in model methods
  • Use Decimal for monetary values

Patterns to Avoid

  • Calculations in views
  • Direct SQL without ORM
  • Skipping validation with raw update()

File Map

FilePurpose
finance/models/init.pyModel exports
finance/models/billing.pyMain billing model (1119 lines)
finance/models/claim.pyInsurance claims
finance/models/claim_response.pyERA processing
finance/models/bill_approval_action.pyApproval workflow
finance/models/insurance_group.pyPayer configuration
finance/models/invoice.pyB2B invoicing
finance/models/transactions.pyPayment transactions
finance/proxies/Proxy models
finance/views/API views (23 files)
finance/constants.pyConstants

On this page