ServicesCrelio AppArchitectureApp Modules

communication

SMS, WhatsApp, Email notification orchestration and multi-channel messaging

Communication App Architecture

Domain Responsibility

What This App Owns

  • CommunicationBase mixin for all notification-enabled models
  • Trigger configuration - When to send notifications
  • Template management - Message templates per channel
  • Provider abstraction - Twilio, Pinnacle, Interakt, etc.
  • B2B communication - Organization-level notifications

What It Depends On

  • core/ - BaseModel, Fusion client
  • admin/ - Labs, settings, organizations

What Should NOT Be Added Here

  • Domain-specific notification triggers (belongs in domain apps)
  • External API integrations (belongs in integration/)
  • User management (belongs in admin/)

Model-Centric Design (Fat Models)

Key Models

ModelResponsibilityKey Fields
CommunicationVariantTemplate definitionstrigger_id, template, channel
CommunicationVariantRelationTemplate-trigger mappingvariant_id, trigger_id
CommunicationVariantLabRelationLab-specific overrideslab_id, variant_id, is_active
CommunicationBusinessAccountProvider credentialsprovider, api_key, lab_id

CommunicationBase Mixin

The CommunicationBase class is a mixin that adds notification capabilities to any model.

Location: communication/base.py - 763 lines

Key Methods:

MethodPurposeLines
send()Main entry point103-167
b2b_send()Organization notifications169-215
validate_triggers()Filter enabled triggers272-307
prepare_user_meta()User contact info338-365
prepare_lab_meta()Lab details367-397
prepare_reports_meta()Report context402-483
send_otp()OTP sending

Usage Pattern:

# Model inherits CommunicationBase (via composition or mixin)
class UserDetails(BaseModel):
    should_send_notifications = True
    
    def after_save(self, *args, **kwargs):
        if self.should_send_notifications:
            self.send(lab_id=self.labId_id, ...)

Provider Services

Located in communication/services/:

ServiceFilePurpose
SMSservices/sms.pySMS provider abstraction
WhatsAppservices/whatsapp.pyWhatsApp provider abstraction
Emailservices/email.pyEmail sending

WhatsAppCommunication (communication/services/whatsapp.py) - 580 lines

Provider Methods:

MethodProvider
prepare_twilio_payload()Twilio
prepare_pinnacle_payload()Pinnacle
prepare_interakt_payload()Interakt
prepare_bevetal_payload()Bevetal (Saudi)
prepare_exacall_payload()Exacall

Why Fat Model Style Here

Communication logic is a cross-cutting concern:

  1. Multiple models need notifications
  2. Trigger logic is complex (lab settings, user consent, B2B rules)
  3. Multi-channel orchestration

Using a mixin keeps notification logic consistent.


Communication Flow

Standard Notification Flow

Trigger Types

Triggers are defined in config files:

# communication/services/config/sms.py
SMS_TRIGGERS = {
    REGISTRATION: 1,
    REPORT_READY: 2,
    BILL_RECEIPT: 3,
    APPOINTMENT_REMINDER: 4,
    ...
}

# communication/services/config/whatsapp.py
WHATSAPP_TRIGGERS = {
    REPORT_READY: 10,
    COLLECTION_SCHEDULED: 11,
    ...
}

Data Access Patterns

Template Resolution

# communication/base.py
def prepare_template(self, trigger_id):
    """Get template for trigger, with lab override"""
    
    # Check lab-specific override
    lab_template = CommunicationVariantLabRelation.objects.filter(
        lab_id=self.lab_id,
        trigger_id=trigger_id,
        is_active=True
    ).first()
    
    if lab_template:
        return lab_template.variant.template
    
    # Fall back to default
    return CommunicationVariantRelation.objects.get(
        trigger_id=trigger_id
    ).variant.template

Business Account Lookup

def prepare_service_config(self):
    """Get provider credentials for lab"""
    
    business_account = CommunicationBusinessAccount.objects.filter(
        lab_id=self.lab_id,
        channel="whatsapp",
        is_active=True
    ).first()
    
    if not business_account:
        # Use platform default
        return self.get_default_config()
    
    return business_account.config

API Layer (Wiring)

Endpoint Map

EndpointViewPurpose
GET /communication/templatesTemplateListViewList templates
POST /communication/templateTemplateCreateViewCreate template
POST /communication/send/otpOtpViewOTP sending
POST /communication/webhookDeliveryWebhookProvider callbacks

Management Commands

# communication/management/commands/
├── sync_templates.py    # Sync templates from config
└── test_sms.py          # Test SMS delivery

Integrations

Provider Integration

ProviderChannelRegion
TwilioSMS + WhatsAppGlobal
PinnacleWhatsAppIndia
InteraktWhatsAppIndia
BevetalWhatsAppSaudi
ExacallWhatsAppMulti
SendGridEmailGlobal

Fusion Worker Integration

All communications go through Fusion:

# core/utils/fusion/client.py
class FusionClient:
    def queue_communication(self, sms=None, whatsapp=None, email=None):
        """Queue communication job"""
        return self.queue_job("send_communication", {
            "sms": sms,
            "whatsapp": whatsapp,
            "email": email
        })

Side Effects & Hidden Coupling

Hard Stop Flags

# communication/base.py
def check_hard_stop(self):
    """Check if communication should be blocked"""
    
    # Lab-level disable
    if self.lab_settings.get("disable_all_communications"):
        return True
    
    # User consent
    if not self.user_meta.get("sms_consent"):
        self.should_trigger_sms = False
    
    return False

Cross-App Coupling

ImportUsed For
admin.account.models.labsLab settings
admin.account.proxies.lab_settingFeature flags
patient.models.user_detailsContact info

Performance & Scaling Notes

Optimization

  • Templates are cached per lab
  • Provider configs cached
  • Bulk sending via Fusion batch jobs

Queue Considerations

  • SMS/WhatsApp go to same queue by default
  • High-priority messages (OTP) can use separate queue
  • Delivery callbacks are async

Safe Extension Guide

Adding New Communication Channel

  1. Add service in services/{channel}.py:

    class NewChannelCommunication:
        def send_message(self, trigger_id, config):
            ...
        
        def prepare_payload(self, trigger_id, config):
            ...
  2. Register in CommunicationBase:

    communication_flags = {
        "should_trigger_new_channel": NEW_CHANNEL_ID,
        ...
    }
  3. Add trigger config in services/config/{channel}.py

Adding New Provider

  1. Add to existing service file:

    # services/whatsapp.py
    def prepare_new_provider_payload(self, trigger_id, config):
        ...
  2. Register provider name in prepare_service_config()

Patterns to Follow

  • Always queue via Fusion (no sync sending)
  • Check consent before sending
  • Log all communication attempts

Patterns to Avoid

  • Synchronous provider calls
  • Hardcoded phone numbers/templates
  • Skipping consent checks

File Map

FilePurpose
communication/base.pyCommunicationBase mixin (763 lines)
communication/services/sms.pySMS service (10315 lines)
communication/services/whatsapp.pyWhatsApp service (580 lines)
communication/services/email.pyEmail service
communication/services/config/Trigger configs
communication/models/Data models (24 files)
communication/proxies/Proxy models
communication/views/API views
communication/urls.pyURL routing

On this page