""" Deadline notification and alert service Handles automated deadline reminders and notifications with workflow integration """ from typing import List, Dict, Any, Optional from datetime import datetime, date, timezone, timedelta from sqlalchemy.orm import Session, joinedload from sqlalchemy import and_, func, or_, desc from app.models import ( Deadline, DeadlineReminder, User, Employee, DeadlineStatus, DeadlinePriority, NotificationFrequency ) from app.services.deadlines import DeadlineService from app.services.workflow_integration import log_deadline_approaching_sync from app.utils.logging import app_logger logger = app_logger class DeadlineNotificationService: """Service for managing deadline notifications and alerts""" def __init__(self, db: Session): self.db = db self.deadline_service = DeadlineService(db) def process_daily_reminders(self, notification_date: date = None) -> Dict[str, Any]: """Process all reminders that should be sent today""" if notification_date is None: notification_date = date.today() logger.info(f"Processing deadline reminders for {notification_date}") # First, check for approaching deadlines and trigger workflow events workflow_events_triggered = self.check_approaching_deadlines_for_workflows(notification_date) # Get pending reminders for today pending_reminders = self.deadline_service.get_pending_reminders(notification_date) results = { "date": notification_date, "total_reminders": len(pending_reminders), "sent_successfully": 0, "failed": 0, "workflow_events_triggered": workflow_events_triggered, "errors": [] } for reminder in pending_reminders: try: # Send the notification success = self._send_reminder_notification(reminder) if success: # Mark as sent self.deadline_service.mark_reminder_sent( reminder.id, delivery_status="sent" ) results["sent_successfully"] += 1 logger.info(f"Sent reminder {reminder.id} for deadline '{reminder.deadline.title}'") else: # Mark as failed self.deadline_service.mark_reminder_sent( reminder.id, delivery_status="failed", error_message="Failed to send notification" ) results["failed"] += 1 results["errors"].append(f"Failed to send reminder {reminder.id}") except Exception as e: # Mark as failed with error self.deadline_service.mark_reminder_sent( reminder.id, delivery_status="failed", error_message=str(e) ) results["failed"] += 1 results["errors"].append(f"Error sending reminder {reminder.id}: {str(e)}") logger.error(f"Error processing reminder {reminder.id}: {str(e)}") logger.info(f"Reminder processing complete: {results['sent_successfully']} sent, {results['failed']} failed, {workflow_events_triggered} workflow events triggered") return results def check_approaching_deadlines_for_workflows(self, check_date: date = None) -> int: """Check for approaching deadlines and trigger workflow events""" if check_date is None: check_date = date.today() # Get deadlines approaching within the next 7 days end_date = check_date + timedelta(days=7) approaching_deadlines = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date.between(check_date, end_date) ).options( joinedload(Deadline.file), joinedload(Deadline.client) ).all() events_triggered = 0 for deadline in approaching_deadlines: try: # Calculate days until deadline days_until = (deadline.deadline_date - check_date).days # Determine deadline type for workflow context deadline_type = getattr(deadline, 'deadline_type', None) deadline_type_str = deadline_type.value if deadline_type else 'other' # Log workflow event for deadline approaching log_deadline_approaching_sync( db=self.db, deadline_id=deadline.id, file_no=deadline.file_no, client_id=deadline.client_id, days_until_deadline=days_until, deadline_type=deadline_type_str ) events_triggered += 1 logger.debug(f"Triggered workflow event for deadline {deadline.id} '{deadline.title}' ({days_until} days away)") except Exception as e: logger.error(f"Error triggering workflow event for deadline {deadline.id}: {str(e)}") if events_triggered > 0: logger.info(f"Triggered {events_triggered} deadline approaching workflow events") return events_triggered def get_urgent_alerts( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> List[Dict[str, Any]]: """Get urgent deadline alerts that need immediate attention""" today = date.today() # Build base query for urgent deadlines query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) # Get overdue and critical upcoming deadlines urgent_deadlines = query.filter( or_( # Overdue deadlines Deadline.deadline_date < today, # Critical priority deadlines due within 3 days and_( Deadline.priority == DeadlinePriority.CRITICAL, Deadline.deadline_date <= today + timedelta(days=3) ), # High priority deadlines due within 1 day and_( Deadline.priority == DeadlinePriority.HIGH, Deadline.deadline_date <= today + timedelta(days=1) ) ) ).options( joinedload(Deadline.file), joinedload(Deadline.client), joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee) ).order_by( Deadline.deadline_date.asc(), Deadline.priority.desc() ).all() alerts = [] for deadline in urgent_deadlines: alert_level = self._determine_alert_level(deadline, today) alerts.append({ "deadline_id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "deadline_time": deadline.deadline_time, "priority": deadline.priority.value, "deadline_type": deadline.deadline_type.value, "alert_level": alert_level, "days_until_deadline": deadline.days_until_deadline, "is_overdue": deadline.is_overdue, "file_no": deadline.file_no, "client_name": self._get_client_name(deadline), "assigned_to": self._get_assigned_to(deadline), "court_name": deadline.court_name, "case_number": deadline.case_number }) return alerts def get_dashboard_summary( self, user_id: Optional[int] = None, employee_id: Optional[str] = None ) -> Dict[str, Any]: """Get deadline summary for dashboard display""" today = date.today() # Build base query query = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING ) if user_id: query = query.filter(Deadline.assigned_to_user_id == user_id) if employee_id: query = query.filter(Deadline.assigned_to_employee_id == employee_id) # Calculate counts overdue_count = query.filter(Deadline.deadline_date < today).count() due_today_count = query.filter(Deadline.deadline_date == today).count() due_tomorrow_count = query.filter( Deadline.deadline_date == today + timedelta(days=1) ).count() due_this_week_count = query.filter( Deadline.deadline_date.between( today, today + timedelta(days=7) ) ).count() due_next_week_count = query.filter( Deadline.deadline_date.between( today + timedelta(days=8), today + timedelta(days=14) ) ).count() # Critical priority counts critical_overdue = query.filter( Deadline.priority == DeadlinePriority.CRITICAL, Deadline.deadline_date < today ).count() critical_upcoming = query.filter( Deadline.priority == DeadlinePriority.CRITICAL, Deadline.deadline_date.between(today, today + timedelta(days=7)) ).count() return { "overdue": overdue_count, "due_today": due_today_count, "due_tomorrow": due_tomorrow_count, "due_this_week": due_this_week_count, "due_next_week": due_next_week_count, "critical_overdue": critical_overdue, "critical_upcoming": critical_upcoming, "total_pending": query.count(), "needs_attention": overdue_count + critical_overdue + critical_upcoming } def create_adhoc_reminder( self, deadline_id: int, recipient_user_id: int, reminder_date: date, custom_message: Optional[str] = None ) -> DeadlineReminder: """Create an ad-hoc reminder for a specific deadline""" deadline = self.db.query(Deadline).filter(Deadline.id == deadline_id).first() if not deadline: raise ValueError(f"Deadline {deadline_id} not found") recipient = self.db.query(User).filter(User.id == recipient_user_id).first() if not recipient: raise ValueError(f"User {recipient_user_id} not found") # Calculate days before deadline days_before = (deadline.deadline_date - reminder_date).days reminder = DeadlineReminder( deadline_id=deadline_id, reminder_date=reminder_date, days_before_deadline=days_before, recipient_user_id=recipient_user_id, recipient_email=recipient.email if hasattr(recipient, 'email') else None, subject=f"Custom Reminder: {deadline.title}", message=custom_message or f"Custom reminder for deadline '{deadline.title}' due on {deadline.deadline_date}", notification_method="email" ) self.db.add(reminder) self.db.commit() self.db.refresh(reminder) logger.info(f"Created ad-hoc reminder {reminder.id} for deadline {deadline_id}") return reminder def get_notification_preferences(self, user_id: int) -> Dict[str, Any]: """Get user's notification preferences (placeholder for future implementation)""" # This would be expanded to include user-specific notification settings # For now, return default preferences return { "email_enabled": True, "in_app_enabled": True, "sms_enabled": False, "advance_notice_days": { "critical": 7, "high": 3, "medium": 1, "low": 1 }, "notification_times": ["09:00", "17:00"], # When to send daily notifications "quiet_hours": { "start": "18:00", "end": "08:00" } } def schedule_court_date_reminders( self, deadline_id: int, court_date: date, preparation_days: int = 7 ): """Schedule special reminders for court dates with preparation milestones""" deadline = self.db.query(Deadline).filter(Deadline.id == deadline_id).first() if not deadline: raise ValueError(f"Deadline {deadline_id} not found") recipient_user_id = deadline.assigned_to_user_id or deadline.created_by_user_id # Schedule preparation milestone reminders preparation_milestones = [ (preparation_days, "Begin case preparation"), (3, "Final preparation and document review"), (1, "Last-minute preparation and travel arrangements"), (0, "Court appearance today") ] for days_before, milestone_message in preparation_milestones: reminder_date = court_date - timedelta(days=days_before) if reminder_date >= date.today(): reminder = DeadlineReminder( deadline_id=deadline_id, reminder_date=reminder_date, days_before_deadline=days_before, recipient_user_id=recipient_user_id, subject=f"Court Date Preparation: {deadline.title}", message=f"{milestone_message} - Court appearance on {court_date}", notification_method="email" ) self.db.add(reminder) self.db.commit() logger.info(f"Scheduled court date reminders for deadline {deadline_id}") # Private helper methods def _send_reminder_notification(self, reminder: DeadlineReminder) -> bool: """Send a reminder notification (placeholder for actual implementation)""" try: # In a real implementation, this would: # 1. Format the notification message # 2. Send via email/SMS/push notification # 3. Handle delivery confirmations # 4. Retry failed deliveries # For now, just log the notification logger.info( f"NOTIFICATION: {reminder.subject} to user {reminder.recipient_user_id} " f"for deadline '{reminder.deadline.title}' due {reminder.deadline.deadline_date}" ) # Simulate successful delivery return True except Exception as e: logger.error(f"Failed to send notification: {str(e)}") return False def _determine_alert_level(self, deadline: Deadline, today: date) -> str: """Determine the alert level for a deadline""" days_until = deadline.days_until_deadline if deadline.is_overdue: return "critical" if deadline.priority == DeadlinePriority.CRITICAL: if days_until <= 1: return "critical" elif days_until <= 3: return "high" else: return "medium" elif deadline.priority == DeadlinePriority.HIGH: if days_until <= 0: return "critical" elif days_until <= 1: return "high" else: return "medium" else: if days_until <= 0: return "high" else: return "low" def _get_client_name(self, deadline: Deadline) -> Optional[str]: """Get formatted client name from deadline""" if deadline.client: return f"{deadline.client.first or ''} {deadline.client.last or ''}".strip() elif deadline.file and deadline.file.owner: return f"{deadline.file.owner.first or ''} {deadline.file.owner.last or ''}".strip() return None def _get_assigned_to(self, deadline: Deadline) -> Optional[str]: """Get assigned person name from deadline""" if deadline.assigned_to_user: return deadline.assigned_to_user.username elif deadline.assigned_to_employee: employee = deadline.assigned_to_employee return f"{employee.first_name or ''} {employee.last_name or ''}".strip() return None class DeadlineAlertManager: """Manager for deadline alert workflows and automation""" def __init__(self, db: Session): self.db = db self.notification_service = DeadlineNotificationService(db) def run_daily_alert_processing(self, process_date: date = None) -> Dict[str, Any]: """Run the daily deadline alert processing workflow""" if process_date is None: process_date = date.today() logger.info(f"Starting daily deadline alert processing for {process_date}") results = { "process_date": process_date, "reminders_processed": {}, "urgent_alerts_generated": 0, "errors": [] } try: # Process scheduled reminders reminder_results = self.notification_service.process_daily_reminders(process_date) results["reminders_processed"] = reminder_results # Generate urgent alerts for overdue items urgent_alerts = self.notification_service.get_urgent_alerts() results["urgent_alerts_generated"] = len(urgent_alerts) # Log summary logger.info( f"Daily processing complete: {reminder_results['sent_successfully']} reminders sent, " f"{results['urgent_alerts_generated']} urgent alerts generated" ) except Exception as e: error_msg = f"Error in daily alert processing: {str(e)}" results["errors"].append(error_msg) logger.error(error_msg) return results def escalate_overdue_deadlines( self, escalation_days: int = 1 ) -> List[Dict[str, Any]]: """Escalate deadlines that have been overdue for specified days""" cutoff_date = date.today() - timedelta(days=escalation_days) overdue_deadlines = self.db.query(Deadline).filter( Deadline.status == DeadlineStatus.PENDING, Deadline.deadline_date <= cutoff_date ).options( joinedload(Deadline.file), joinedload(Deadline.assigned_to_user), joinedload(Deadline.assigned_to_employee) ).all() escalations = [] for deadline in overdue_deadlines: # Create escalation record escalation = { "deadline_id": deadline.id, "title": deadline.title, "deadline_date": deadline.deadline_date, "days_overdue": (date.today() - deadline.deadline_date).days, "priority": deadline.priority.value, "assigned_to": self.notification_service._get_assigned_to(deadline), "file_no": deadline.file_no, "escalation_date": date.today() } escalations.append(escalation) # In a real system, this would: # 1. Send escalation notifications to supervisors # 2. Create escalation tasks # 3. Update deadline status if needed logger.warning( f"ESCALATION: Deadline '{deadline.title}' (ID: {deadline.id}) " f"overdue by {escalation['days_overdue']} days" ) return escalations