ServicesCrelio AppArchitectureApp Modules

patient

Patient registration, demographics, home collection, and patient-facing features

Patient App Architecture

Domain Responsibility

What This App Owns

  • Patient registration and demographics (UserDetails)
  • Home collection scheduling (HomeCollection)
  • Patient insurance management (PatientInsurance)
  • Patient portal access (via proxies)
  • ID proof management (UserIdProofDetails)
  • Appointment scheduling (Appointment)

What It Depends On

  • core/ - BaseModel, utilities
  • admin/ - Labs, lab users, settings
  • finance/ - Billing integration
  • report/ - Lab report access
  • communication/ - Notifications

What Should NOT Be Added Here

  • Lab report generation logic (belongs in report/)
  • Billing/payment logic (belongs in finance/)
  • Lab operations (belongs in operation/)

Model-Centric Design (Fat Models)

Key Models

ModelResponsibilityKey FieldsCross-App Relations
UserDetailsCore patient entityfullName, contact, dateOfBirth, labUserIdfinance.Billing, report.LabReportRelation
HomeCollectionHome sample pickupscheduled_date, phlebotomist, statusadmin.LabUser
PatientInsuranceInsurance infopolicy_number, insurance_providerfinance.UserBillInsurance
UserIdProofDetailsID documentsproof_type, document_pathadmin.Attachments
AppointmentDoctor appointmentsappointment_date, doctor_idadmin.Doctors

Business Logic in Models

UserDetails (patient/models/user_details.py) - 3342 lines

Lifecycle Methods:

MethodLinesPurpose
validate()653-678Orchestrate all validations
before_save()249-268Prepare context, generate IDs
after_save()454-507ES sync, webhooks, notifications

Validation Methods:

MethodPurpose
validate_patient_action()Check user permissions
validate_strict_check()Field-level access control
validate_contact_info()Phone/email format
validate_age()DOB and age format
validate_national_id()National ID format
validate_insurance()Insurance details

Business Operations:

MethodPurpose
register_patient(payload, is_collection_center, session)New patient workflow
merge_patient(merge_details, existing_patient_id, session)Merge patient records
add_relative(details, relative_patient_id, session)Add family member
transfer_patient_records(session, merge_to, bill_ids, ...)De-merge/transfer
process_dependent_patients()Sync dependent records

Side Effect Methods:

MethodPurpose
update_es_record()Sync to Elasticsearch
trigger_patient_webhooks()Fire webhooks
trigger_notifications()Queue SMS/WhatsApp
sync_patient_lab_reports_to_es()Sync related reports

Why Fat Model Style Here

UserDetails is the central patient entity. All patient operations flow through it:

  1. Validation ensures data quality
  2. before_save() generates IDs, formats data
  3. after_save() handles external sync

Logic is NOT in views because:

  • Same logic needed from API, mobile, and integrations
  • Consistent validation across all entry points
  • Side effects are traceable in one place

Invariants & Data Rules

Patient Constraints

ConstraintEnforcementLocation
Unique labUserId per labbefore_save() generationgenerate_lab_wise_sequential_patient_id()
Valid phone formatvalidate_contact_info()Validation
Age from DOBComputed in before_save()set_default_values()
Country code required with contactvalidate_contact_info()Validation

State Transitions

FieldValuesTransition Logic
isActive0/1Manual via update
syncStatusVariousUpdated on ES sync
mergedPatientIdPatient ID or NoneSet on merge

Data Access Patterns

QuerySet Locations

PatternLocationExample
Model classmethodUserDetails.get_patient()Fetch with validation
Proxy modelPatientReport.get_reports()Domain-specific queries
ManagerPatientOverviewManagerComplex joins

Optimization Patterns

# patient/managers/patient_overview_manager.py
def get_patient_overview(self, patient_id, lab_id):
    return UserDetails.objects.filter(
        id=patient_id, labId_id=lab_id
    ).select_related(
        "user", "labId", "orgId"
    ).prefetch_related(
        "billing_set", "labreportrelation_set"
    )

N+1 Risk Zones

QueryRiskMitigation
Patient list with billsLoading billing_set per patientprefetch_related
Patient with reportsLoading reports per billBatch or lazy load

API Layer (Wiring)

Endpoint Map

EndpointViewModel Method
POST /patient/registerRegistrationView.post()UserDetails.register_patient()
POST /patient/{id}/id-proofsIdProofsView.post()patient.save_id_proofs()
POST /patient/mergePatientMergeView.post()UserDetails.merge_patient()
POST /patient/{id}/demergePatientDeMergeView.post()patient.transfer_patient_records()
POST /patient/relativeAddPatientRelativeView.post()UserDetails.add_relative()

Thin View Pattern

# patient/views/registration.py
class RegistrationView(View):
    def post(self, request, *args, **kwargs):
        # 1. Extract session/context
        lab_id = self.get_lab_id_from_session(request.session)
        payload = request.data
        
        # 2. Prepare data (minimal)
        payload = self.prepare_mandatory_patient_data(payload)
        
        # 3. Delegate to model
        user_details = UserDetails.register_patient(
            payload=payload,
            is_collection_center=is_collection_center,
            session=request.session,
        )
        
        # 4. Return response
        return JsonResponse({"patient": model_to_dict(user_details)})

Permission Checks

PermissionSession KeyChecked In
Add patientuserAddNewPatientFlagvalidate_patient_action()
Update patientuserUpdatePatientFlagvalidate_patient_action()
Merge patientmerge_patient_flagView level
De-merge patientdemerge_patientView level

Integrations

External Systems

IntegrationDirectionHandler
ElasticsearchOutboundupdate_es_record()
WebhooksOutboundtrigger_patient_webhooks()
Crelio AIOutboundtrigger_crelio_ai_webhook()
HL7Outboundprepare_hl7_payload()
SMS/WhatsAppOutboundVia CommunicationBase mixin

Webhook Trigger Points

# patient/models/user_details.py
def trigger_patient_webhooks(self, hl7_payload, **kwargs):
    """Fire webhooks for patient events"""
    # Integration webhooks
    WebhookIntegrationManager.trigger_webhook(...)

Side Effects & Hidden Coupling

after_save() Side Effects

Side EffectLocationRisk
ES syncupdate_es_record()Sync - may fail
Webhooktrigger_patient_webhooks()Async via Fusion
Notificationstrigger_notifications()Async via Fusion
Dependent patientsprocess_dependent_patients()DB updates

Cross-App Coupling

ImportUsed ForRisk Level
finance.models.billing.BillingBill creation checkMedium
report.models.lab_report_relation.LabReportRelationReport syncMedium
admin.account.proxies.lab_setting.LabSettingFeature flagsLow
communication.base.CommunicationBaseNotificationsLow

[!WARNING] UserDetails imports from 20+ modules across 8 apps. This is a coupling hotspot.


Performance & Scaling Notes

Hot Tables

TableSize ConcernIndexes
userDetailsHigh volumelabId_id, contact, labUserId
patientInsuranceMany per patientuserDetailsId_id

Query Optimization

# Common query pattern
UserDetails.objects.filter(
    labId_id=lab_id,
    contact=contact
).select_related("user", "labId")

Caching

  • Patient instances cached in Redis if allow_individual_instance_caching = True
  • Lab settings cached for feature flags

Safe Extension Guide

Adding New Patient Attribute

  1. Add field to UserDetails model
  2. Add migration
  3. Add validation in validate() if needed
  4. Add to ES sync in update_es_record() if searchable

Adding New Patient Workflow

  1. Add method to UserDetails:

    @classmethod
    def new_workflow(cls, payload, session):
        instance = cls.objects.get(pk=payload["patient_id"])
        instance.set_values(payload)
        instance.save(session=session)
        return instance
  2. Create thin view:

    class NewWorkflowView(GenericView):
        @transaction.atomic
        def post(self, request, *args, **kwargs):
            result = UserDetails.new_workflow(request.data, request.session)
            return JsonResponse({"status": "success"})

Patterns to Follow

  • All business logic in UserDetails methods
  • Use @transaction.atomic in views
  • Validate permissions in validate_* methods

Patterns to Avoid

  • Business logic in views
  • Direct ES calls from views
  • Skipping model validation with update() for critical fields

File Map

FilePurpose
patient/models/user_details.pyMain patient model (3342 lines)
patient/models/patient_insurance.pyInsurance management
patient/models/user_id_proof_details.pyID documents
patient/models/additional_patient_info.pyExtended fields
patient/models/home_collection/Home collection models
patient/models/appointment/Appointment models
patient/proxies/patient_report.pyPatient-facing report access (991 lines)
patient/proxies/lab_patient.pyLab-side patient operations
patient/views/registration.pyRegistration views (375 lines)
patient/managers/patient_overview_manager.pyOverview queries
patient/utils.pyHelper functions

On this page