292 lines
17 KiB
Python
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
|