ServicesCrelio AppArchitectureApp Modules patient
Patient registration, demographics, home collection, and patient-facing features
- 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)
core/ - BaseModel, utilities
admin/ - Labs, lab users, settings
finance/ - Billing integration
report/ - Lab report access
communication/ - Notifications
- Lab report generation logic (belongs in
report/)
- Billing/payment logic (belongs in
finance/)
- Lab operations (belongs in
operation/)
| Model | Responsibility | Key Fields | Cross-App Relations |
|---|
UserDetails | Core patient entity | fullName, contact, dateOfBirth, labUserId | finance.Billing, report.LabReportRelation |
HomeCollection | Home sample pickup | scheduled_date, phlebotomist, status | admin.LabUser |
PatientInsurance | Insurance info | policy_number, insurance_provider | finance.UserBillInsurance |
UserIdProofDetails | ID documents | proof_type, document_path | admin.Attachments |
Appointment | Doctor appointments | appointment_date, doctor_id | admin.Doctors |
Lifecycle Methods:
| Method | Lines | Purpose |
|---|
validate() | 653-678 | Orchestrate all validations |
before_save() | 249-268 | Prepare context, generate IDs |
after_save() | 454-507 | ES sync, webhooks, notifications |
Validation Methods:
| Method | Purpose |
|---|
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:
| Method | Purpose |
|---|
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:
| Method | Purpose |
|---|
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 |
UserDetails is the central patient entity. All patient operations flow through it:
- Validation ensures data quality
before_save() generates IDs, formats data
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
| Constraint | Enforcement | Location |
|---|
Unique labUserId per lab | before_save() generation | generate_lab_wise_sequential_patient_id() |
| Valid phone format | validate_contact_info() | Validation |
| Age from DOB | Computed in before_save() | set_default_values() |
| Country code required with contact | validate_contact_info() | Validation |
| Field | Values | Transition Logic |
|---|
isActive | 0/1 | Manual via update |
syncStatus | Various | Updated on ES sync |
mergedPatientId | Patient ID or None | Set on merge |
| Pattern | Location | Example |
|---|
| Model classmethod | UserDetails.get_patient() | Fetch with validation |
| Proxy model | PatientReport.get_reports() | Domain-specific queries |
| Manager | PatientOverviewManager | Complex joins |
# 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"
)
| Query | Risk | Mitigation |
|---|
| Patient list with bills | Loading billing_set per patient | prefetch_related |
| Patient with reports | Loading reports per bill | Batch or lazy load |
| Endpoint | View | Model Method |
|---|
POST /patient/register | RegistrationView.post() | UserDetails.register_patient() |
POST /patient/{id}/id-proofs | IdProofsView.post() | patient.save_id_proofs() |
POST /patient/merge | PatientMergeView.post() | UserDetails.merge_patient() |
POST /patient/{id}/demerge | PatientDeMergeView.post() | patient.transfer_patient_records() |
POST /patient/relative | AddPatientRelativeView.post() | UserDetails.add_relative() |
# 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 | Session Key | Checked In |
|---|
| Add patient | userAddNewPatientFlag | validate_patient_action() |
| Update patient | userUpdatePatientFlag | validate_patient_action() |
| Merge patient | merge_patient_flag | View level |
| De-merge patient | demerge_patient | View level |
| Integration | Direction | Handler |
|---|
| Elasticsearch | Outbound | update_es_record() |
| Webhooks | Outbound | trigger_patient_webhooks() |
| Crelio AI | Outbound | trigger_crelio_ai_webhook() |
| HL7 | Outbound | prepare_hl7_payload() |
| SMS/WhatsApp | Outbound | Via CommunicationBase mixin |
# patient/models/user_details.py
def trigger_patient_webhooks(self, hl7_payload, **kwargs):
"""Fire webhooks for patient events"""
# Integration webhooks
WebhookIntegrationManager.trigger_webhook(...)
| Side Effect | Location | Risk |
|---|
| ES sync | update_es_record() | Sync - may fail |
| Webhook | trigger_patient_webhooks() | Async via Fusion |
| Notifications | trigger_notifications() | Async via Fusion |
| Dependent patients | process_dependent_patients() | DB updates |
| Import | Used For | Risk Level |
|---|
finance.models.billing.Billing | Bill creation check | Medium |
report.models.lab_report_relation.LabReportRelation | Report sync | Medium |
admin.account.proxies.lab_setting.LabSetting | Feature flags | Low |
communication.base.CommunicationBase | Notifications | Low |
[!WARNING]
UserDetails imports from 20+ modules across 8 apps. This is a coupling hotspot.
| Table | Size Concern | Indexes |
|---|
userDetails | High volume | labId_id, contact, labUserId |
patientInsurance | Many per patient | userDetailsId_id |
# Common query pattern
UserDetails.objects.filter(
labId_id=lab_id,
contact=contact
).select_related("user", "labId")
- Patient instances cached in Redis if
allow_individual_instance_caching = True
- Lab settings cached for feature flags
- Add field to
UserDetails model
- Add migration
- Add validation in
validate() if needed
- Add to ES sync in
update_es_record() if searchable
-
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
-
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"})
- All business logic in
UserDetails methods
- Use
@transaction.atomic in views
- Validate permissions in
validate_* methods
- Business logic in views
- Direct ES calls from views
- Skipping model validation with
update() for critical fields