feat: Implement Step 10-11 - Statistics and Notification Services
Step 10: Data Analytics and Statistics Service - Created comprehensive statistics service with real-time metrics collection - Implemented time-series data storage interface (InfluxDB compatible) - Added data aggregation and analytics endpoints - Integrated Redis caching for performance optimization - Made Kafka connection optional for resilience Step 11: Real-time Notification System - Built multi-channel notification service (Email, SMS, Push, In-App) - Implemented priority-based queue management with Redis - Created template engine for dynamic notifications - Added user preference management for personalized notifications - Integrated WebSocket server for real-time updates - Fixed pymongo/motor compatibility issues (motor 3.5.1) Testing: - Created comprehensive test suites for both services - Added integration test script to verify cross-service communication - All services passing health checks and functional tests 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
375
services/notifications/backend/notification_manager.py
Normal file
375
services/notifications/backend/notification_manager.py
Normal file
@ -0,0 +1,375 @@
|
||||
"""
|
||||
Notification Manager - Core notification orchestration
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
import uuid
|
||||
from models import (
|
||||
Notification, NotificationChannel, NotificationStatus,
|
||||
NotificationPriority, NotificationHistory, NotificationPreference
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class NotificationManager:
|
||||
"""Manages notification creation, delivery, and tracking"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
channel_handlers: Dict[NotificationChannel, Any],
|
||||
queue_manager: Any,
|
||||
template_engine: Any,
|
||||
preference_manager: Any
|
||||
):
|
||||
self.channel_handlers = channel_handlers
|
||||
self.queue_manager = queue_manager
|
||||
self.template_engine = template_engine
|
||||
self.preference_manager = preference_manager
|
||||
self.is_running = False
|
||||
self.notification_store = {} # In-memory store for demo
|
||||
self.history_store = [] # In-memory history for demo
|
||||
self.device_tokens = {} # In-memory device tokens for demo
|
||||
|
||||
async def start(self):
|
||||
"""Start notification manager"""
|
||||
self.is_running = True
|
||||
# Start background tasks for processing queued notifications
|
||||
asyncio.create_task(self._process_notification_queue())
|
||||
asyncio.create_task(self._process_scheduled_notifications())
|
||||
logger.info("Notification manager started")
|
||||
|
||||
async def stop(self):
|
||||
"""Stop notification manager"""
|
||||
self.is_running = False
|
||||
logger.info("Notification manager stopped")
|
||||
|
||||
async def create_notification(
|
||||
self,
|
||||
user_id: str,
|
||||
title: str,
|
||||
message: str,
|
||||
channels: List[NotificationChannel],
|
||||
priority: NotificationPriority = NotificationPriority.NORMAL,
|
||||
data: Optional[Dict[str, Any]] = None,
|
||||
template_id: Optional[str] = None,
|
||||
schedule_at: Optional[datetime] = None
|
||||
) -> Notification:
|
||||
"""Create a new notification"""
|
||||
|
||||
# Check user preferences
|
||||
preferences = await self.preference_manager.get_user_preferences(user_id)
|
||||
if preferences:
|
||||
# Filter channels based on user preferences
|
||||
channels = [ch for ch in channels if preferences.channels.get(ch, True)]
|
||||
|
||||
# Apply template if provided
|
||||
if template_id:
|
||||
template = await self.template_engine.get_template(template_id)
|
||||
if template:
|
||||
message = await self.template_engine.render_template(template, data or {})
|
||||
|
||||
# Create notification objects for each channel
|
||||
notification = Notification(
|
||||
id=str(uuid.uuid4()),
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
message=message,
|
||||
channel=channels[0] if channels else NotificationChannel.IN_APP,
|
||||
priority=priority,
|
||||
data=data,
|
||||
template_id=template_id,
|
||||
scheduled_at=schedule_at,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
# Store notification
|
||||
self.notification_store[notification.id] = notification
|
||||
|
||||
logger.info(f"Created notification {notification.id} for user {user_id}")
|
||||
return notification
|
||||
|
||||
async def send_notification(self, notification: Notification):
|
||||
"""Send a single notification"""
|
||||
try:
|
||||
# Check if notification should be sent now
|
||||
if notification.scheduled_at and notification.scheduled_at > datetime.now():
|
||||
await self.queue_manager.schedule_notification(notification, notification.scheduled_at)
|
||||
return
|
||||
|
||||
# Get the appropriate handler
|
||||
handler = self.channel_handlers.get(notification.channel)
|
||||
if not handler:
|
||||
raise ValueError(f"No handler for channel {notification.channel}")
|
||||
|
||||
# Send through the channel
|
||||
success = await handler.send(notification)
|
||||
|
||||
if success:
|
||||
notification.status = NotificationStatus.SENT
|
||||
notification.sent_at = datetime.now()
|
||||
logger.info(f"Notification {notification.id} sent successfully")
|
||||
else:
|
||||
notification.status = NotificationStatus.FAILED
|
||||
notification.retry_count += 1
|
||||
logger.error(f"Failed to send notification {notification.id}")
|
||||
|
||||
# Retry if needed
|
||||
if notification.retry_count < self._get_max_retries(notification.priority):
|
||||
await self.queue_manager.enqueue_notification(notification)
|
||||
|
||||
# Update notification
|
||||
self.notification_store[notification.id] = notification
|
||||
|
||||
# Add to history
|
||||
await self._add_to_history(notification)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending notification {notification.id}: {e}")
|
||||
notification.status = NotificationStatus.FAILED
|
||||
notification.error_message = str(e)
|
||||
self.notification_store[notification.id] = notification
|
||||
|
||||
async def send_bulk_notifications(self, notifications: List[Notification]):
|
||||
"""Send multiple notifications"""
|
||||
tasks = []
|
||||
for notification in notifications:
|
||||
tasks.append(self.send_notification(notification))
|
||||
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
async def mark_as_read(self, notification_id: str) -> bool:
|
||||
"""Mark notification as read"""
|
||||
notification = self.notification_store.get(notification_id)
|
||||
if notification:
|
||||
notification.status = NotificationStatus.READ
|
||||
notification.read_at = datetime.now()
|
||||
self.notification_store[notification_id] = notification
|
||||
logger.info(f"Notification {notification_id} marked as read")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def delete_notification(self, notification_id: str) -> bool:
|
||||
"""Delete a notification"""
|
||||
if notification_id in self.notification_store:
|
||||
del self.notification_store[notification_id]
|
||||
logger.info(f"Notification {notification_id} deleted")
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_notification(self, notification_id: str) -> Optional[Notification]:
|
||||
"""Get a notification by ID"""
|
||||
return self.notification_store.get(notification_id)
|
||||
|
||||
async def get_user_notifications(
|
||||
self,
|
||||
user_id: str,
|
||||
status: Optional[NotificationStatus] = None,
|
||||
channel: Optional[NotificationChannel] = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[Notification]:
|
||||
"""Get notifications for a user"""
|
||||
notifications = []
|
||||
|
||||
for notification in self.notification_store.values():
|
||||
if notification.user_id != user_id:
|
||||
continue
|
||||
if status and notification.status != status:
|
||||
continue
|
||||
if channel and notification.channel != channel:
|
||||
continue
|
||||
notifications.append(notification)
|
||||
|
||||
# Sort by created_at descending
|
||||
notifications.sort(key=lambda x: x.created_at, reverse=True)
|
||||
|
||||
# Apply pagination
|
||||
return notifications[offset:offset + limit]
|
||||
|
||||
async def retry_notification(self, notification: Notification):
|
||||
"""Retry a failed notification"""
|
||||
notification.retry_count += 1
|
||||
notification.status = NotificationStatus.PENDING
|
||||
notification.error_message = None
|
||||
await self.send_notification(notification)
|
||||
|
||||
async def get_notification_history(
|
||||
self,
|
||||
user_id: Optional[str] = None,
|
||||
channel: Optional[NotificationChannel] = None,
|
||||
status: Optional[NotificationStatus] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: int = 100
|
||||
) -> List[NotificationHistory]:
|
||||
"""Get notification history"""
|
||||
history = []
|
||||
|
||||
for entry in self.history_store:
|
||||
if user_id and entry.user_id != user_id:
|
||||
continue
|
||||
if channel and entry.channel != channel:
|
||||
continue
|
||||
if status and entry.status != status:
|
||||
continue
|
||||
if start_date and entry.sent_at and entry.sent_at < start_date:
|
||||
continue
|
||||
if end_date and entry.sent_at and entry.sent_at > end_date:
|
||||
continue
|
||||
history.append(entry)
|
||||
|
||||
# Sort by sent_at descending and limit
|
||||
history.sort(key=lambda x: x.sent_at or datetime.min, reverse=True)
|
||||
return history[:limit]
|
||||
|
||||
async def get_analytics(self, start_date: datetime, end_date: datetime) -> Dict[str, Any]:
|
||||
"""Get notification analytics"""
|
||||
total_sent = 0
|
||||
total_delivered = 0
|
||||
total_read = 0
|
||||
total_failed = 0
|
||||
channel_stats = {}
|
||||
|
||||
for notification in self.notification_store.values():
|
||||
if notification.created_at < start_date or notification.created_at > end_date:
|
||||
continue
|
||||
|
||||
if notification.status == NotificationStatus.SENT:
|
||||
total_sent += 1
|
||||
elif notification.status == NotificationStatus.DELIVERED:
|
||||
total_delivered += 1
|
||||
elif notification.status == NotificationStatus.READ:
|
||||
total_read += 1
|
||||
elif notification.status == NotificationStatus.FAILED:
|
||||
total_failed += 1
|
||||
|
||||
# Channel stats
|
||||
channel_name = notification.channel.value
|
||||
if channel_name not in channel_stats:
|
||||
channel_stats[channel_name] = {
|
||||
"sent": 0,
|
||||
"delivered": 0,
|
||||
"read": 0,
|
||||
"failed": 0
|
||||
}
|
||||
|
||||
if notification.status == NotificationStatus.SENT:
|
||||
channel_stats[channel_name]["sent"] += 1
|
||||
elif notification.status == NotificationStatus.DELIVERED:
|
||||
channel_stats[channel_name]["delivered"] += 1
|
||||
elif notification.status == NotificationStatus.READ:
|
||||
channel_stats[channel_name]["read"] += 1
|
||||
elif notification.status == NotificationStatus.FAILED:
|
||||
channel_stats[channel_name]["failed"] += 1
|
||||
|
||||
total = total_sent + total_delivered + total_read + total_failed
|
||||
|
||||
return {
|
||||
"period": f"{start_date.isoformat()} to {end_date.isoformat()}",
|
||||
"total_notifications": total,
|
||||
"total_sent": total_sent,
|
||||
"total_delivered": total_delivered,
|
||||
"total_read": total_read,
|
||||
"total_failed": total_failed,
|
||||
"delivery_rate": (total_delivered / total * 100) if total > 0 else 0,
|
||||
"read_rate": (total_read / total * 100) if total > 0 else 0,
|
||||
"channel_stats": channel_stats
|
||||
}
|
||||
|
||||
async def register_device_token(
|
||||
self,
|
||||
user_id: str,
|
||||
device_token: str,
|
||||
device_type: str
|
||||
) -> bool:
|
||||
"""Register a device token for push notifications"""
|
||||
if user_id not in self.device_tokens:
|
||||
self.device_tokens[user_id] = []
|
||||
|
||||
# Check if token already exists
|
||||
for token in self.device_tokens[user_id]:
|
||||
if token["token"] == device_token:
|
||||
# Update existing token
|
||||
token["device_type"] = device_type
|
||||
token["updated_at"] = datetime.now()
|
||||
return True
|
||||
|
||||
# Add new token
|
||||
self.device_tokens[user_id].append({
|
||||
"token": device_token,
|
||||
"device_type": device_type,
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now()
|
||||
})
|
||||
|
||||
logger.info(f"Registered device token for user {user_id}")
|
||||
return True
|
||||
|
||||
async def unregister_device_token(self, device_token: str) -> bool:
|
||||
"""Unregister a device token"""
|
||||
for user_id, tokens in self.device_tokens.items():
|
||||
for i, token in enumerate(tokens):
|
||||
if token["token"] == device_token:
|
||||
del self.device_tokens[user_id][i]
|
||||
logger.info(f"Unregistered device token for user {user_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_max_retries(self, priority: NotificationPriority) -> int:
|
||||
"""Get max retries based on priority"""
|
||||
retry_map = {
|
||||
NotificationPriority.LOW: 1,
|
||||
NotificationPriority.NORMAL: 3,
|
||||
NotificationPriority.HIGH: 5,
|
||||
NotificationPriority.URGENT: 10
|
||||
}
|
||||
return retry_map.get(priority, 3)
|
||||
|
||||
async def _add_to_history(self, notification: Notification):
|
||||
"""Add notification to history"""
|
||||
history_entry = NotificationHistory(
|
||||
notification_id=notification.id,
|
||||
user_id=notification.user_id,
|
||||
channel=notification.channel,
|
||||
status=notification.status,
|
||||
title=notification.title,
|
||||
message=notification.message,
|
||||
sent_at=notification.sent_at,
|
||||
delivered_at=notification.delivered_at,
|
||||
read_at=notification.read_at,
|
||||
error_message=notification.error_message,
|
||||
metadata={"priority": notification.priority.value}
|
||||
)
|
||||
self.history_store.append(history_entry)
|
||||
|
||||
async def _process_notification_queue(self):
|
||||
"""Process queued notifications"""
|
||||
while self.is_running:
|
||||
try:
|
||||
# Get notification from queue
|
||||
notification_data = await self.queue_manager.dequeue_notification()
|
||||
if notification_data:
|
||||
notification = Notification(**notification_data)
|
||||
await self.send_notification(notification)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing notification queue: {e}")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _process_scheduled_notifications(self):
|
||||
"""Process scheduled notifications"""
|
||||
while self.is_running:
|
||||
try:
|
||||
# Check for scheduled notifications
|
||||
now = datetime.now()
|
||||
for notification in self.notification_store.values():
|
||||
if (notification.scheduled_at and
|
||||
notification.scheduled_at <= now and
|
||||
notification.status == NotificationStatus.PENDING):
|
||||
await self.send_notification(notification)
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing scheduled notifications: {e}")
|
||||
|
||||
await asyncio.sleep(10) # Check every 10 seconds
|
||||
Reference in New Issue
Block a user