Files
delphi-database/app/services/deadline_notifications.py
HotSwapp bac8cc4bd5 changes
2025-08-18 20:20:04 -05:00

536 lines
20 KiB
Python

"""
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