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 clientadmin/- 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
| Model | Responsibility | Key Fields |
|---|---|---|
CommunicationVariant | Template definitions | trigger_id, template, channel |
CommunicationVariantRelation | Template-trigger mapping | variant_id, trigger_id |
CommunicationVariantLabRelation | Lab-specific overrides | lab_id, variant_id, is_active |
CommunicationBusinessAccount | Provider credentials | provider, 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:
| Method | Purpose | Lines |
|---|---|---|
send() | Main entry point | 103-167 |
b2b_send() | Organization notifications | 169-215 |
validate_triggers() | Filter enabled triggers | 272-307 |
prepare_user_meta() | User contact info | 338-365 |
prepare_lab_meta() | Lab details | 367-397 |
prepare_reports_meta() | Report context | 402-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/:
| Service | File | Purpose |
|---|---|---|
| SMS | services/sms.py | SMS provider abstraction |
services/whatsapp.py | WhatsApp provider abstraction | |
services/email.py | Email sending |
WhatsAppCommunication (communication/services/whatsapp.py) - 580 lines
Provider Methods:
| Method | Provider |
|---|---|
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:
- Multiple models need notifications
- Trigger logic is complex (lab settings, user consent, B2B rules)
- 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.templateBusiness 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.configAPI Layer (Wiring)
Endpoint Map
| Endpoint | View | Purpose |
|---|---|---|
GET /communication/templates | TemplateListView | List templates |
POST /communication/template | TemplateCreateView | Create template |
POST /communication/send/otp | OtpView | OTP sending |
POST /communication/webhook | DeliveryWebhook | Provider callbacks |
Management Commands
# communication/management/commands/
├── sync_templates.py # Sync templates from config
└── test_sms.py # Test SMS deliveryIntegrations
Provider Integration
| Provider | Channel | Region |
|---|---|---|
| Twilio | SMS + WhatsApp | Global |
| Pinnacle | India | |
| Interakt | India | |
| Bevetal | Saudi | |
| Exacall | Multi | |
| SendGrid | Global |
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 FalseCross-App Coupling
| Import | Used For |
|---|---|
admin.account.models.labs | Lab settings |
admin.account.proxies.lab_setting | Feature flags |
patient.models.user_details | Contact 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
-
Add service in
services/{channel}.py:class NewChannelCommunication: def send_message(self, trigger_id, config): ... def prepare_payload(self, trigger_id, config): ... -
Register in
CommunicationBase:communication_flags = { "should_trigger_new_channel": NEW_CHANNEL_ID, ... } -
Add trigger config in
services/config/{channel}.py
Adding New Provider
-
Add to existing service file:
# services/whatsapp.py def prepare_new_provider_payload(self, trigger_id, config): ... -
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
| File | Purpose |
|---|---|
| communication/base.py | CommunicationBase mixin (763 lines) |
| communication/services/sms.py | SMS service (10315 lines) |
| communication/services/whatsapp.py | WhatsApp service (580 lines) |
| communication/services/email.py | Email service |
| communication/services/config/ | Trigger configs |
| communication/models/ | Data models (24 files) |
| communication/proxies/ | Proxy models |
| communication/views/ | API views |
| communication/urls.py | URL routing |