nexus-5/core/services/email_renderer.py
2026-01-26 11:09:40 -05:00

292 lines
17 KiB
Python

"""
HTML email rendering service for notifications.
Renders notification data into branded HTML emails using Django templates.
"""
from datetime import datetime
from typing import Dict, Any, List
from django.template.loader import render_to_string
# Event type to display configuration mapping
# Colors match the frontend brand palette (layout.css)
EVENT_TYPE_CONFIG: Dict[str, Dict[str, str]] = {
# Customer events - Primary Blue
'CUSTOMER_CREATED': {'label': 'Customer Created', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_UPDATED': {'label': 'Customer Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_DELETED': {'label': 'Customer Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'CUSTOMER_STATUS_CHANGED': {'label': 'Status Changed', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_ADDRESS_CREATED': {'label': 'Address Added', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_ADDRESS_UPDATED': {'label': 'Address Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_ADDRESS_DELETED': {'label': 'Address Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'CUSTOMER_CONTACT_CREATED': {'label': 'Contact Added', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_CONTACT_UPDATED': {'label': 'Contact Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CUSTOMER_CONTACT_DELETED': {'label': 'Contact Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Account events - Primary Blue
'ACCOUNT_CREATED': {'label': 'Account Created', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_UPDATED': {'label': 'Account Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_DELETED': {'label': 'Account Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'ACCOUNT_STATUS_CHANGED': {'label': 'Status Changed', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_ADDRESS_CREATED': {'label': 'Address Added', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_ADDRESS_UPDATED': {'label': 'Address Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_ADDRESS_DELETED': {'label': 'Address Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'ACCOUNT_CONTACT_CREATED': {'label': 'Contact Added', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_CONTACT_UPDATED': {'label': 'Contact Updated', 'color': '#3b78c4', 'bg': '#3b78c420'},
'ACCOUNT_CONTACT_DELETED': {'label': 'Contact Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Service events - Secondary Green / Red for cancel
'SERVICE_CREATED': {'label': 'Service Scheduled', 'color': '#458c5e', 'bg': '#458c5e20'},
'SERVICE_UPDATED': {'label': 'Service Updated', 'color': '#458c5e', 'bg': '#458c5e20'},
'SERVICE_DELETED': {'label': 'Service Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SERVICE_STATUS_CHANGED': {'label': 'Status Changed', 'color': '#458c5e', 'bg': '#458c5e20'},
'SERVICE_COMPLETED': {'label': 'Service Completed', 'color': '#22c546', 'bg': '#22c54620'},
'SERVICE_CANCELLED': {'label': 'Service Cancelled', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SERVICE_TEAM_ASSIGNED': {'label': 'Team Assigned', 'color': '#458c5e', 'bg': '#458c5e20'},
'SERVICE_TEAM_UNASSIGNED': {'label': 'Team Unassigned', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'SERVICE_DISPATCHED': {'label': 'Service Dispatched', 'color': '#3b78c4', 'bg': '#3b78c420'},
'SERVICES_BULK_GENERATED': {'label': 'Services Generated', 'color': '#458c5e', 'bg': '#458c5e20'},
# Service session events
'SERVICE_SESSION_OPENED': {'label': 'Session Started', 'color': '#458c5e', 'bg': '#458c5e20'},
'SERVICE_SESSION_CLOSED': {'label': 'Session Completed', 'color': '#22c546', 'bg': '#22c54620'},
'SERVICE_SESSION_REVERTED': {'label': 'Session Reverted', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'SERVICE_TASK_COMPLETED': {'label': 'Task Completed', 'color': '#22c546', 'bg': '#22c54620'},
'SERVICE_TASK_UNCOMPLETED': {'label': 'Task Uncompleted', 'color': '#d8a01d', 'bg': '#d8a01d20'},
# Schedule events - Cyan
'SCHEDULE_CREATED': {'label': 'Schedule Created', 'color': '#0891b2', 'bg': '#0891b220'},
'SCHEDULE_UPDATED': {'label': 'Schedule Updated', 'color': '#0891b2', 'bg': '#0891b220'},
'SCHEDULE_DELETED': {'label': 'Schedule Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SCHEDULE_FREQUENCY_CHANGED': {'label': 'Frequency Changed', 'color': '#0891b2', 'bg': '#0891b220'},
# Project events - Orange
'PROJECT_CREATED': {'label': 'Project Created', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_UPDATED': {'label': 'Project Updated', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_STATUS_CHANGED': {'label': 'Status Changed', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_COMPLETED': {'label': 'Project Completed', 'color': '#22c546', 'bg': '#22c54620'},
'PROJECT_CANCELLED': {'label': 'Project Cancelled', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PROJECT_DISPATCHED': {'label': 'Project Dispatched', 'color': '#3b78c4', 'bg': '#3b78c420'},
# Project session events
'PROJECT_SESSION_OPENED': {'label': 'Session Started', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SESSION_CLOSED': {'label': 'Session Completed', 'color': '#22c546', 'bg': '#22c54620'},
'PROJECT_SESSION_REVERTED': {'label': 'Session Reverted', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'PROJECT_TASK_COMPLETED': {'label': 'Task Completed', 'color': '#22c546', 'bg': '#22c54620'},
'PROJECT_TASK_UNCOMPLETED': {'label': 'Task Uncompleted', 'color': '#d8a01d', 'bg': '#d8a01d20'},
# Project scope events - Orange
'PROJECT_SCOPE_CREATED': {'label': 'Scope Created', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_UPDATED': {'label': 'Scope Updated', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_DELETED': {'label': 'Scope Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PROJECT_SCOPE_CATEGORY_CREATED': {'label': 'Category Added', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_CATEGORY_UPDATED': {'label': 'Category Updated', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_CATEGORY_DELETED': {'label': 'Category Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PROJECT_SCOPE_TASK_CREATED': {'label': 'Task Added', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_TASK_UPDATED': {'label': 'Task Updated', 'color': '#e16a36', 'bg': '#e16a3620'},
'PROJECT_SCOPE_TASK_DELETED': {'label': 'Task Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PROJECT_SCOPE_TEMPLATE_INSTANTIATED': {'label': 'Template Applied', 'color': '#e16a36', 'bg': '#e16a3620'},
# Scope events - Accent3 (teal-ish)
'SCOPE_CREATED': {'label': 'Scope Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'SCOPE_UPDATED': {'label': 'Scope Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'SCOPE_DELETED': {'label': 'Scope Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'AREA_CREATED': {'label': 'Area Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'AREA_UPDATED': {'label': 'Area Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'AREA_DELETED': {'label': 'Area Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'TASK_CREATED': {'label': 'Task Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'TASK_UPDATED': {'label': 'Task Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'TASK_DELETED': {'label': 'Task Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'TASK_COMPLETION_RECORDED': {'label': 'Task Completed', 'color': '#22c546', 'bg': '#22c54620'},
# Scope template events
'SCOPE_TEMPLATE_CREATED': {'label': 'Template Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'SCOPE_TEMPLATE_UPDATED': {'label': 'Template Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'SCOPE_TEMPLATE_DELETED': {'label': 'Template Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SCOPE_TEMPLATE_INSTANTIATED': {'label': 'Template Applied', 'color': '#14b8a6', 'bg': '#14b8a620'},
'AREA_TEMPLATE_CREATED': {'label': 'Area Template Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'AREA_TEMPLATE_UPDATED': {'label': 'Area Template Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'AREA_TEMPLATE_DELETED': {'label': 'Area Template Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'TASK_TEMPLATE_CREATED': {'label': 'Task Template Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'TASK_TEMPLATE_UPDATED': {'label': 'Task Template Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'TASK_TEMPLATE_DELETED': {'label': 'Task Template Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Team profile events - Rose
'TEAM_PROFILE_CREATED': {'label': 'Team Member Added', 'color': '#f43f5e', 'bg': '#f43f5e20'},
'TEAM_PROFILE_UPDATED': {'label': 'Profile Updated', 'color': '#f43f5e', 'bg': '#f43f5e20'},
'TEAM_PROFILE_DELETED': {'label': 'Team Member Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'TEAM_PROFILE_ROLE_CHANGED': {'label': 'Role Changed', 'color': '#f43f5e', 'bg': '#f43f5e20'},
# Customer profile events - Teal
'CUSTOMER_PROFILE_CREATED': {'label': 'Access Created', 'color': '#14b8a6', 'bg': '#14b8a620'},
'CUSTOMER_PROFILE_UPDATED': {'label': 'Profile Updated', 'color': '#14b8a6', 'bg': '#14b8a620'},
'CUSTOMER_PROFILE_DELETED': {'label': 'Access Removed', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'CUSTOMER_PROFILE_ACCESS_GRANTED': {'label': 'Access Granted', 'color': '#22c546', 'bg': '#22c54620'},
'CUSTOMER_PROFILE_ACCESS_REVOKED': {'label': 'Access Revoked', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Punchlist events - Warning Yellow
'ACCOUNT_PUNCHLIST_CREATED': {'label': 'Issue Reported', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'ACCOUNT_PUNCHLIST_UPDATED': {'label': 'Issue Updated', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'ACCOUNT_PUNCHLIST_DELETED': {'label': 'Issue Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PROJECT_PUNCHLIST_CREATED': {'label': 'Issue Reported', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'PROJECT_PUNCHLIST_UPDATED': {'label': 'Issue Updated', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'PROJECT_PUNCHLIST_DELETED': {'label': 'Issue Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'PUNCHLIST_STATUS_CHANGED': {'label': 'Issue Status Changed', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'PUNCHLIST_PRIORITY_CHANGED': {'label': 'Priority Changed', 'color': '#d8a01d', 'bg': '#d8a01d20'},
# Session media events - Purple
'SESSION_IMAGE_UPLOADED': {'label': 'Image Uploaded', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_IMAGE_UPDATED': {'label': 'Image Updated', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_IMAGE_DELETED': {'label': 'Image Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SESSION_VIDEO_UPLOADED': {'label': 'Video Uploaded', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_VIDEO_UPDATED': {'label': 'Video Updated', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_VIDEO_DELETED': {'label': 'Video Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'SESSION_MEDIA_INTERNAL_FLAGGED': {'label': 'Media Flagged Internal', 'color': '#d8a01d', 'bg': '#d8a01d20'},
# Session notes events
'SESSION_NOTE_CREATED': {'label': 'Note Added', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_NOTE_UPDATED': {'label': 'Note Updated', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'SESSION_NOTE_DELETED': {'label': 'Note Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Report events - Purple
'REPORT_CREATED': {'label': 'Report Created', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'REPORT_SUBMITTED': {'label': 'Report Submitted', 'color': '#8b6bc2', 'bg': '#8b6bc220'},
'REPORT_APPROVED': {'label': 'Report Approved', 'color': '#22c546', 'bg': '#22c54620'},
# Invoice events - Indigo
'INVOICE_GENERATED': {'label': 'Invoice Generated', 'color': '#6366f1', 'bg': '#6366f120'},
'INVOICE_SENT': {'label': 'Invoice Sent', 'color': '#6366f1', 'bg': '#6366f120'},
'INVOICE_PAID': {'label': 'Invoice Paid', 'color': '#22c546', 'bg': '#22c54620'},
'INVOICE_OVERDUE': {'label': 'Invoice Overdue', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'INVOICE_CANCELLED': {'label': 'Invoice Cancelled', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Labor & Revenue events - Indigo
'LABOR_RATE_CREATED': {'label': 'Labor Rate Created', 'color': '#6366f1', 'bg': '#6366f120'},
'LABOR_RATE_UPDATED': {'label': 'Labor Rate Updated', 'color': '#6366f1', 'bg': '#6366f120'},
'LABOR_RATE_DELETED': {'label': 'Labor Rate Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
'REVENUE_RATE_CREATED': {'label': 'Revenue Rate Created', 'color': '#6366f1', 'bg': '#6366f120'},
'REVENUE_RATE_UPDATED': {'label': 'Revenue Rate Updated', 'color': '#6366f1', 'bg': '#6366f120'},
'REVENUE_RATE_DELETED': {'label': 'Revenue Rate Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
# Messaging events - Primary Blue
'CONVERSATION_CREATED': {'label': 'Conversation Started', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CONVERSATION_ARCHIVED': {'label': 'Conversation Archived', 'color': '#64748b', 'bg': '#64748b20'},
'CONVERSATION_PARTICIPANT_ADDED': {'label': 'Participant Added', 'color': '#3b78c4', 'bg': '#3b78c420'},
'CONVERSATION_PARTICIPANT_REMOVED': {'label': 'Participant Removed', 'color': '#d8a01d', 'bg': '#d8a01d20'},
'MESSAGE_SENT': {'label': 'Message Sent', 'color': '#3b78c4', 'bg': '#3b78c420'},
'MESSAGE_RECEIVED': {'label': 'Message Received', 'color': '#3b78c4', 'bg': '#3b78c420'},
'MESSAGE_READ': {'label': 'Message Read', 'color': '#64748b', 'bg': '#64748b20'},
'MESSAGE_DELETED': {'label': 'Message Deleted', 'color': '#e14a4a', 'bg': '#e14a4a20'},
}
# Default configuration for unknown event types
DEFAULT_EVENT_CONFIG = {'label': 'Notification', 'color': '#3b78c4', 'bg': '#3b78c420'}
# Metadata keys to display in emails (with human-readable labels)
METADATA_DISPLAY_KEYS: Dict[str, str] = {
'account_name': 'Account',
'customer_name': 'Customer',
'project_name': 'Project',
'service_date': 'Service Date',
'date': 'Date',
'scheduled_date': 'Scheduled Date',
'status': 'Status',
'old_status': 'Previous Status',
'new_status': 'New Status',
'invoice_number': 'Invoice #',
'amount': 'Amount',
'team_member_name': 'Team Member',
'month': 'Month',
'count': 'Count',
'old_role': 'Previous Role',
'new_role': 'New Role',
'old_frequency': 'Previous Frequency',
'new_frequency': 'New Frequency',
'priority': 'Priority',
'frequency': 'Frequency',
}
class NotificationEmailRenderer:
"""
Renders notifications as HTML emails using Django templates.
"""
@staticmethod
def render_html(
notification,
recipient_name: str,
recipient_email: str
) -> str:
"""
Render notification as branded HTML email.
Args:
notification: Notification model instance
recipient_name: Display name of the recipient
recipient_email: Email address of the recipient
Returns:
Rendered HTML string ready for sending
"""
event = notification.event
event_type = event.event_type if event else None
# Get event type display configuration
event_config = EVENT_TYPE_CONFIG.get(event_type, DEFAULT_EVENT_CONFIG)
# Build metadata items for display
metadata = event.metadata if event and event.metadata else {}
metadata_items = NotificationEmailRenderer._build_metadata_items(metadata)
context = {
'subject': notification.subject,
'body': notification.body,
'action_url': notification.action_url or '',
'recipient_name': recipient_name,
'recipient_email': recipient_email,
'event_type_label': event_config['label'],
'event_type_color': event_config['color'],
'event_type_bg_color': event_config['bg'],
'metadata_items': metadata_items,
'current_year': datetime.now().year,
}
return render_to_string('email/base_notification.html', context)
@staticmethod
def _build_metadata_items(metadata: Dict[str, Any]) -> List[Dict[str, str]]:
"""
Build list of metadata items for display in the email.
Args:
metadata: Event metadata dictionary
Returns:
List of dicts with 'label' and 'value' keys
"""
items = []
for key, label in METADATA_DISPLAY_KEYS.items():
if key not in metadata or metadata[key] is None:
continue
value = metadata[key]
# Format specific value types
if isinstance(value, bool):
value = 'Yes' if value else 'No'
elif key == 'amount' and isinstance(value, (int, float)):
value = f'${value:,.2f}'
elif key in ('status', 'old_status', 'new_status', 'old_role', 'new_role'):
# Format status/role values (e.g., PENDING -> Pending)
value = str(value).replace('_', ' ').title()
else:
value = str(value)
# Skip empty strings
if value:
items.append({'label': label, 'value': value})
return items