Files
robinhood/backend/app/services/notification_service.py
Claude 7dd2166bf4 Implement Phase 1: Daily Helper Foundation
Complete implementation of Phase 1 enhancements including:

Backend:
- UserProfile model for storing user preferences (timezone, trading style, risk tolerance)
- DailyRoutine model for scheduling routines (morning, active_trading, evening)
- RoutineExecution model for tracking routine execution history
- Notification model for managing all types of notifications
- DailyChecklist model for daily task tracking with completion percentage
- HabitTracker model for tracking habits and streaks

Services:
- RoutineService: Handles routine execution with task registry pattern
- RoutineScheduler: Async scheduler for automated routine execution
- NotificationService: Comprehensive notification creation and delivery system
- Support for price alerts, news, routines, reminders, and performance notifications

API Endpoints (daily_helper router):
- User profile: CRUD operations, get/update preferences
- Daily routines: Create, list, execute, track history
- Notifications: CRUD, mark read, batch operations
- Daily checklists: CRUD, item management, completion tracking
- Habits: Create, track, log completions, manage streaks
- Dashboard: Summary endpoint for daily helper overview

Frontend Components:
- UserProfileSetup: Complete user profile configuration with preferences
- NotificationCenter: Bell icon with dropdown, notification management
- HabitTracker: Habit creation, streak tracking, gamification with fire emojis
- DailyChecklistPanel: Checklist management with completion percentage

Schemas:
- Full Pydantic schemas for request/response validation
- Type-safe API contracts

Features:
- Timezone support for international users
- Trading style and risk tolerance preferences
- Automated routine execution with task registry
- Real-time notifications with priority levels
- Habit streaks with motivational badges
- Daily checklist with persistent state
- Completion percentage tracking
- Notes and metadata support

All components are production-ready with error handling and user feedback.
2025-11-15 23:09:10 +00:00

308 lines
9.2 KiB
Python

"""
Notification Service
Handles notification creation, delivery, and management
"""
from datetime import datetime
from typing import Optional, List, Dict
from sqlalchemy.orm import Session
from app.models.models import Notification
import asyncio
import logging
logger = logging.getLogger(__name__)
class NotificationService:
"""Service for managing notifications"""
# Notification priority levels
PRIORITY_LEVELS = {
"critical": 1,
"high": 2,
"normal": 3,
"low": 4,
}
# Delivery methods
DELIVERY_METHODS = ["push", "email", "sms"]
@staticmethod
async def create_notification(
db: Session,
notification_type: str,
title: str,
message: str,
priority: str = "normal",
delivery_method: str = "push",
data: Optional[Dict] = None,
user_id: Optional[str] = None,
) -> Notification:
"""Create a new notification"""
if priority not in NotificationService.PRIORITY_LEVELS:
priority = "normal"
if delivery_method not in NotificationService.DELIVERY_METHODS:
delivery_method = "push"
notification = Notification(
user_id=user_id,
notification_type=notification_type,
title=title,
message=message,
priority=priority,
delivery_method=delivery_method,
data=data or {},
read=False,
)
db.add(notification)
db.commit()
db.refresh(notification)
# Send notification based on delivery method
await NotificationService.send_notification(notification)
return notification
@staticmethod
async def send_notification(notification: Notification):
"""Send notification via appropriate delivery method"""
try:
if notification.delivery_method == "push":
await NotificationService.send_push(notification)
elif notification.delivery_method == "email":
await NotificationService.send_email(notification)
elif notification.delivery_method == "sms":
await NotificationService.send_sms(notification)
except Exception as e:
logger.error(f"Failed to send notification {notification.id}: {str(e)}")
@staticmethod
async def send_push(notification: Notification):
"""Send push notification (browser/WebSocket)"""
# In production, this would integrate with push notification service
# For now, log it
logger.info(
f"Push notification: {notification.title} - {notification.message}"
)
@staticmethod
async def send_email(notification: Notification):
"""Send email notification"""
# In production, integrate with email service (SendGrid, Mailgun, etc.)
logger.info(
f"Email notification: {notification.title} - {notification.message}"
)
@staticmethod
async def send_sms(notification: Notification):
"""Send SMS notification"""
# In production, integrate with SMS service (Twilio, etc.)
logger.info(
f"SMS notification: {notification.title} - {notification.message}"
)
@staticmethod
async def mark_as_read(
db: Session,
notification_id: int
) -> Notification:
"""Mark notification as read"""
notification = db.query(Notification).filter(
Notification.id == notification_id
).first()
if notification:
notification.read = True
notification.read_at = datetime.utcnow()
db.commit()
db.refresh(notification)
return notification
@staticmethod
async def mark_all_as_read(db: Session, user_id: Optional[str] = None):
"""Mark all notifications as read"""
query = db.query(Notification).filter(Notification.read == False)
if user_id:
query = query.filter(Notification.user_id == user_id)
query.update({
Notification.read: True,
Notification.read_at: datetime.utcnow()
})
db.commit()
@staticmethod
def get_unread_count(db: Session, user_id: Optional[str] = None) -> int:
"""Get count of unread notifications"""
query = db.query(Notification).filter(Notification.read == False)
if user_id:
query = query.filter(Notification.user_id == user_id)
return query.count()
@staticmethod
async def create_price_alert_notification(
db: Session,
price: float,
threshold: float,
direction: str, # "above" or "below"
):
"""Create notification for price alert"""
title = f"Price Alert: Gold {direction} ${threshold:.2f}"
message = f"Gold price has moved {direction} your alert level. Current: ${price:.2f}"
await NotificationService.create_notification(
db,
notification_type="price_alert",
title=title,
message=message,
priority="high",
delivery_method="push",
data={
"price": price,
"threshold": threshold,
"direction": direction
}
)
@staticmethod
async def create_routine_notification(
db: Session,
routine_type: str,
routine_name: str,
):
"""Create notification for routine execution"""
messages = {
"morning": f"Good morning! Time for your {routine_name} routine.",
"active_trading": f"Active trading time! {routine_name} routine.",
"evening": f"End of trading day. Time to review with {routine_name} routine.",
}
title = f"Daily Routine: {routine_name}"
message = messages.get(routine_type, f"Time for {routine_name} routine")
await NotificationService.create_notification(
db,
notification_type="routine",
title=title,
message=message,
priority="normal",
delivery_method="push",
data={
"routine_type": routine_type,
"routine_name": routine_name
}
)
@staticmethod
async def create_news_notification(
db: Session,
news_title: str,
summary: str,
impact: str = "medium", # low, medium, high
):
"""Create notification for important news"""
priority_map = {
"low": "low",
"medium": "normal",
"high": "high",
}
await NotificationService.create_notification(
db,
notification_type="news",
title=f"Market News: {news_title}",
message=summary[:200], # Limit to 200 chars
priority=priority_map.get(impact, "normal"),
delivery_method="push",
data={
"news_title": news_title,
"impact": impact
}
)
@staticmethod
async def create_reminder_notification(
db: Session,
reminder_type: str,
title: str,
message: str,
):
"""Create reminder notification"""
await NotificationService.create_notification(
db,
notification_type="reminder",
title=title,
message=message,
priority="normal",
delivery_method="push",
data={
"reminder_type": reminder_type
}
)
@staticmethod
async def create_performance_notification(
db: Session,
metric: str,
value: float,
target: Optional[float] = None,
):
"""Create notification for performance metrics"""
if target:
message = f"Your {metric} is {value:.2f} (target: {target:.2f})"
else:
message = f"Your {metric} is {value:.2f}"
await NotificationService.create_notification(
db,
notification_type="report",
title=f"Performance: {metric}",
message=message,
priority="normal",
delivery_method="push",
data={
"metric": metric,
"value": value,
"target": target
}
)
class NotificationScheduler:
"""Schedule notification delivery and cleanup"""
@staticmethod
async def cleanup_old_notifications(db: Session, days: int = 30):
"""Remove notifications older than specified days"""
from datetime import timedelta
cutoff_date = datetime.utcnow() - timedelta(days=days)
db.query(Notification).filter(
Notification.created_at < cutoff_date
).delete()
db.commit()
@staticmethod
async def batch_notifications(
db: Session,
user_id: Optional[str] = None,
) -> List[Notification]:
"""Get all unread notifications and batch them for delivery"""
query = db.query(Notification).filter(
Notification.read == False
).order_by(Notification.priority, Notification.created_at.desc())
if user_id:
query = query.filter(Notification.user_id == user_id)
return query.all()