bmc_hub/app/services/reminder_notification_service.py
Christian 30d1be61eb feat: Add global search functionality and email results section
- Introduced a global search button and modal for enhanced user experience.
- Added a new section for displaying email results in the global search modal.
- Implemented functionality to fetch and display emails based on user queries.
- Updated the UI to include a reminders button and improved accessibility features.

fix: Update docker-compose to allow reload configuration

- Changed ENABLE_RELOAD environment variable to default to true for easier development.

chore: Update requirements for new dependencies

- Added brother_ql, pyzbar, and pypdfium2 to requirements for label printing and PDF processing.

feat: Implement Brother label printing service

- Created a new service for printing labels using Brother QL printers.
- Supports direct printing of case hardware labels with customizable layouts.

feat: Add Vaultwarden service for credential management

- Implemented a service to interact with Vaultwarden for secure credential storage and retrieval.

sql: Add migrations for email thread keys and document tokens

- Created migrations to backfill email thread keys and manage document tokens for work orders.
- Introduced new tables and updated existing structures to support token-based linking of scanned documents.

sql: Import links into the database

- Added a script to import a predefined set of links into the database with associated categories.
2026-04-01 21:34:58 +02:00

412 lines
15 KiB
Python

"""
Reminder Notification Service
Handles multi-channel delivery of reminders (Mattermost, Email, Frontend)
Includes rate limiting and user preference merging
"""
import logging
import asyncio
from typing import List, Dict, Optional, Tuple
from datetime import datetime, timedelta
import json
from jinja2 import Template
from app.core.config import settings
from app.core.database import execute_query, execute_insert
from app.services.email_service import EmailService
from app.backups.backend.notifications import MattermostNotification
logger = logging.getLogger(__name__)
class ReminderNotificationService:
"""Service for sending reminders via multiple notification channels"""
def __init__(self):
self.email_service = EmailService()
self.mattermost_service = MattermostNotification()
self.dry_run = settings.REMINDERS_DRY_RUN
self.max_per_hour = settings.REMINDERS_MAX_PER_USER_PER_HOUR
async def send_reminder(
self,
reminder_id: int,
sag_id: int,
case_title: str,
customer_name: str,
reminder_title: str,
reminder_message: Optional[str],
recipient_user_ids: List[int],
recipient_emails: List[str],
priority: str = "normal",
notify_mattermost: Optional[bool] = None,
notify_email: Optional[bool] = None,
notify_frontend: Optional[bool] = None,
override_user_preferences: bool = False,
additional_info: Optional[str] = None,
case_status: Optional[str] = None,
deadline: Optional[str] = None,
assigned_user: Optional[str] = None
) -> Dict[str, any]:
"""
Send reminder via configured notification channels
Returns dict with:
{
'success': bool,
'channels_used': List[str],
'errors': List[str],
'rate_limited_users': List[int],
'logged_id': int
}
"""
result = {
'success': False,
'channels_used': [],
'errors': [],
'rate_limited_users': [],
'logged_id': None
}
if self.dry_run:
logger.warning(f"🔒 DRY RUN: Would send reminder '{reminder_title}' for case #{sag_id}")
# Get case details
case_query = "SELECT id, titel FROM sag_sager WHERE id = %s"
case = execute_query(case_query, (sag_id,))
if not case:
logger.error(f"❌ Case #{sag_id} not found")
result['errors'].append(f"Case #{sag_id} not found")
return result
# Process each recipient user
for user_id in recipient_user_ids:
try:
# Check rate limit
if not await self._check_rate_limit(user_id):
logger.warning(f"⚠️ Rate limit exceeded for user {user_id}")
result['rate_limited_users'].append(user_id)
continue
# Get user preferences
user_prefs = await self._get_user_preferences(user_id)
# Merge with reminder-specific overrides
channels = self._determine_channels(
user_prefs,
notify_mattermost,
notify_email,
notify_frontend,
override_user_preferences
)
# Get user email
user_query = "SELECT email FROM users WHERE user_id = %s"
user = execute_query(user_query, (user_id,))
user_email = user[0]['email'] if user else None
# Send via channels
for channel in channels:
try:
if channel == 'mattermost' and settings.REMINDERS_MATTERMOST_ENABLED:
await self._send_mattermost(
reminder_title, reminder_message, sag_id, case_title,
priority, additional_info
)
result['channels_used'].append('mattermost')
elif channel == 'email' and settings.REMINDERS_EMAIL_ENABLED and user_email:
await self._send_email(
user_email, reminder_title, reminder_message,
sag_id, case_title, customer_name, priority,
case_status, deadline, assigned_user, additional_info
)
result['channels_used'].append('email')
elif channel == 'frontend':
# Frontend notifications are handled by polling, no action needed here
result['channels_used'].append('frontend')
except Exception as e:
error = f"Failed to send via {channel}: {str(e)}"
logger.error(f"{error}")
result['errors'].append(error)
# Log notification
log_id = await self._log_reminder(
reminder_id, sag_id, user_id, result['channels_used'],
{
'title': reminder_title,
'message': reminder_message,
'case': case_title,
'priority': priority
},
'sent' if result['channels_used'] else 'failed'
)
result['logged_id'] = log_id
except Exception as e:
logger.error(f"❌ Error sending reminder to user {user_id}: {e}")
result['errors'].append(f"User {user_id}: {str(e)}")
# Process additional email addresses
for email_addr in recipient_emails:
try:
if settings.REMINDERS_EMAIL_ENABLED:
await self._send_email(
email_addr, reminder_title, reminder_message,
sag_id, case_title, customer_name, priority,
case_status, deadline, assigned_user, additional_info
)
result['channels_used'].append('email')
except Exception as e:
error = f"Failed to send email to {email_addr}: {str(e)}"
logger.error(f"{error}")
result['errors'].append(error)
result['success'] = len(result['errors']) == 0 or len(result['channels_used']) > 0
return result
async def _check_rate_limit(self, user_id: int) -> bool:
"""Check if user has exceeded notification limit (max 5 per hour)"""
query = """
SELECT COUNT(*) as count
FROM sag_reminder_logs
WHERE user_id = %s
AND triggered_at > CURRENT_TIMESTAMP - INTERVAL '1 hour'
AND status = 'sent'
"""
result = execute_query(query, (user_id,))
count = result[0]['count'] if result else 0
if count >= self.max_per_hour:
logger.warning(f"⚠️ Rate limit reached for user {user_id}: {count}/{self.max_per_hour} notifications")
return False
return True
async def _get_user_preferences(self, user_id: int) -> Dict:
"""Get user notification preferences"""
query = """
SELECT notify_mattermost, notify_email, notify_frontend
FROM user_notification_preferences
WHERE user_id = %s
"""
result = execute_query(query, (user_id,))
if result:
return {
'mattermost': result[0].get('notify_mattermost', True),
'email': result[0].get('notify_email', False),
'frontend': result[0].get('notify_frontend', True)
}
# Default preferences
return {
'mattermost': True,
'email': False,
'frontend': True
}
def _determine_channels(
self,
user_prefs: Dict,
notify_mattermost: Optional[bool],
notify_email: Optional[bool],
notify_frontend: Optional[bool],
override: bool
) -> List[str]:
"""Determine which channels to use (merge user prefs with reminder overrides)"""
channels = []
# Mattermost
mm = notify_mattermost if notify_mattermost is not None else user_prefs.get('mattermost', True)
if mm:
channels.append('mattermost')
# Email
em = notify_email if notify_email is not None else user_prefs.get('email', False)
if em:
channels.append('email')
# Frontend
fe = notify_frontend if notify_frontend is not None else user_prefs.get('frontend', True)
if fe:
channels.append('frontend')
return channels
async def _send_mattermost(
self,
title: str,
message: Optional[str],
case_id: int,
case_title: str,
priority: str,
additional_info: Optional[str]
) -> bool:
"""Send reminder via Mattermost"""
if self.dry_run:
logger.warning(f"🔒 DRY RUN: Mattermost reminder '{title}'")
return True
try:
color_map = {
'low': '#6c757d',
'normal': '#0f4c75',
'high': '#ffc107',
'urgent': '#dc3545'
}
payload = {
'text': f'🔔 **{title}**',
'attachments': [{
'title': case_title,
'title_link': f"http://localhost:8000/sag/{case_id}",
'text': message or additional_info or 'Se reminder i systemet',
'color': color_map.get(priority, color_map['normal']),
'fields': [
{
'title': 'Prioritet',
'value': priority.capitalize(),
'short': True
},
{
'title': 'Sag ID',
'value': f'#{case_id}',
'short': True
}
],
'actions': [{
'name': 'Åbn sag',
'type': 'button',
'text': 'Se mere',
'url': f"http://localhost:8000/sag/{case_id}"
}]
}]
}
success, msg = await self.mattermost_service._send_webhook(payload, 'reminder_notification')
if success:
logger.info(f"✅ Mattermost reminder sent: {title}")
else:
logger.error(f"❌ Mattermost error: {msg}")
return success
except Exception as e:
logger.error(f"❌ Mattermost send failed: {e}")
return False
async def _send_email(
self,
to_email: str,
title: str,
message: Optional[str],
case_id: int,
case_title: str,
customer_name: str,
priority: str,
case_status: Optional[str],
deadline: Optional[str],
assigned_user: Optional[str],
additional_info: Optional[str]
) -> bool:
"""Send reminder via email"""
if self.dry_run:
logger.warning(f"🔒 DRY RUN: Email reminder to {to_email}")
return True
try:
# Load email template
with open('templates/emails/reminder.html', 'r') as f:
template_html = f.read()
# Prepare context
context = {
'header_title': 'Reminder: ' + title,
'reminder_title': title,
'reminder_message': message or '',
'case_id': case_id,
'case_title': case_title,
'customer_name': customer_name,
'case_status': case_status or 'Unknown',
'priority': priority,
'deadline': deadline,
'assigned_user': assigned_user or 'Ikke tildelt',
'additional_info': additional_info or '',
'action_url': f"http://localhost:8000/sag/{case_id}",
'footer_date': datetime.now().strftime("%d. %B %Y")
}
# Render template
template = Template(template_html)
body_html = template.render(context)
body_text = f"{title}\n\n{message or ''}\n\nSag: {case_title} (#{case_id})\nKunde: {customer_name}"
# Send email
success, msg = await self.email_service.send_email(
to_addresses=[to_email],
subject=f"[{priority.upper()}] Reminder: {title}",
body_text=body_text,
body_html=body_html,
reply_to=settings.EMAIL_SMTP_FROM_ADDRESS
)
if success:
logger.info(f"✅ Email reminder sent to {to_email}")
else:
logger.error(f"❌ Email error: {msg}")
return success
except Exception as e:
logger.error(f"❌ Email send failed: {e}")
return False
async def _log_reminder(
self,
reminder_id: int,
sag_id: int,
user_id: int,
channels: List[str],
payload: Dict,
status: str = 'sent'
) -> Optional[int]:
"""Log reminder execution"""
try:
query = """
INSERT INTO sag_reminder_logs (
reminder_id, sag_id, user_id,
channels_used, notification_payload,
status, triggered_at
)
VALUES (%s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP)
RETURNING id
"""
result = execute_insert(query, (
reminder_id,
sag_id,
user_id,
channels,
json.dumps(payload),
status
))
if result:
log_id = result[0]['id'] if isinstance(result[0], dict) else result[0]
logger.info(f"📝 Reminder logged (ID: {log_id})")
return log_id
except Exception as e:
logger.error(f"❌ Failed to log reminder: {e}")
return None
# Global instance
reminder_notification_service = ReminderNotificationService()