""" Reminder API Endpoints for Sag Module CRUD operations, user preferences, snooze/dismiss functionality """ import logging from typing import List, Optional from datetime import datetime, timedelta from fastapi import APIRouter, HTTPException, status, Depends, Request from pydantic import BaseModel, Field from app.core.database import execute_query, execute_insert from app.services.reminder_notification_service import reminder_notification_service logger = logging.getLogger(__name__) router = APIRouter() # ============================================================================ # Helper Functions # ============================================================================ def _get_user_id_from_request(request: Request) -> int: """Extract user_id from request query params or raise 401""" user_id = getattr(request.state, 'user_id', None) if user_id is not None: try: return int(user_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid user_id format") user_id = request.query_params.get('user_id') if user_id: try: return int(user_id) except ValueError: raise HTTPException(status_code=400, detail="Invalid user_id format") raise HTTPException(status_code=401, detail="User not authenticated - provide user_id query parameter") # ============================================================================ # Pydantic Schemas # ============================================================================ class UserNotificationPreferences(BaseModel): """User notification preferences""" notify_mattermost: bool = True notify_email: bool = False notify_frontend: bool = True email_override: Optional[str] = None quiet_hours_enabled: bool = False quiet_hours_start: Optional[str] = None # HH:MM format quiet_hours_end: Optional[str] = None # HH:MM format class ReminderCreate(BaseModel): """Create reminder request""" title: str = Field(..., min_length=3, max_length=255) message: Optional[str] = None priority: str = Field(default="normal", pattern="^(low|normal|high|urgent)$") trigger_type: str = Field(pattern="^(status_change|deadline_approaching|time_based)$") trigger_config: dict # JSON config for trigger recipient_user_ids: List[int] = [] recipient_emails: List[str] = [] notify_mattermost: Optional[bool] = None notify_email: Optional[bool] = None notify_frontend: Optional[bool] = None override_user_preferences: bool = False recurrence_type: str = Field(default="once", pattern="^(once|daily|weekly|monthly)$") recurrence_day_of_week: Optional[int] = None # 0-6 for weekly recurrence_day_of_month: Optional[int] = None # 1-31 for monthly scheduled_at: Optional[datetime] = None class ReminderUpdate(BaseModel): """Update reminder request""" title: Optional[str] = None message: Optional[str] = None priority: Optional[str] = None notify_mattermost: Optional[bool] = None notify_email: Optional[bool] = None notify_frontend: Optional[bool] = None override_user_preferences: Optional[bool] = None is_active: Optional[bool] = None class ReminderResponse(BaseModel): """Reminder response""" id: int sag_id: int title: str message: Optional[str] priority: str trigger_type: str recurrence_type: str is_active: bool next_check_at: Optional[datetime] last_sent_at: Optional[datetime] created_at: datetime class ReminderProfileResponse(BaseModel): """Reminder response for profile list""" id: int sag_id: int title: str message: Optional[str] priority: str trigger_type: str recurrence_type: str is_active: bool next_check_at: Optional[datetime] last_sent_at: Optional[datetime] created_at: datetime case_title: Optional[str] customer_name: Optional[str] class ReminderLogResponse(BaseModel): """Reminder execution log""" id: int reminder_id: Optional[int] sag_id: int status: str triggered_at: datetime channels_used: List[str] class SnoozeRequest(BaseModel): """Snooze reminder request""" duration_minutes: int = Field(..., ge=15, le=1440) # 15 min to 24 hours class DismissRequest(BaseModel): """Dismiss reminder request""" reason: Optional[str] = None # ============================================================================ # User Preferences Endpoints # ============================================================================ @router.get("/api/v1/users/me/notification-preferences", response_model=UserNotificationPreferences) async def get_user_notification_preferences(request: Request): """Get current user's notification preferences""" user_id = _get_user_id_from_request(request) query = """ SELECT notify_mattermost, notify_email, notify_frontend, email_override, quiet_hours_enabled, quiet_hours_start, quiet_hours_end FROM user_notification_preferences WHERE user_id = %s """ result = execute_query(query, (user_id,)) if result: r = result[0] return UserNotificationPreferences( notify_mattermost=r.get('notify_mattermost', True), notify_email=r.get('notify_email', False), notify_frontend=r.get('notify_frontend', True), email_override=r.get('email_override'), quiet_hours_enabled=r.get('quiet_hours_enabled', False), quiet_hours_start=r.get('quiet_hours_start'), quiet_hours_end=r.get('quiet_hours_end') ) # Return defaults return UserNotificationPreferences() @router.patch("/api/v1/users/me/notification-preferences") async def update_user_notification_preferences( request: Request, preferences: UserNotificationPreferences ): """Update user notification preferences""" user_id = _get_user_id_from_request(request) # Check if preferences exist check_query = "SELECT id FROM user_notification_preferences WHERE user_id = %s" exists = execute_query(check_query, (user_id,)) try: if exists: # Update existing query = """ UPDATE user_notification_preferences SET notify_mattermost = %s, notify_email = %s, notify_frontend = %s, email_override = %s, quiet_hours_enabled = %s, quiet_hours_start = %s, quiet_hours_end = %s, updated_at = CURRENT_TIMESTAMP WHERE user_id = %s RETURNING id """ execute_insert(query, ( preferences.notify_mattermost, preferences.notify_email, preferences.notify_frontend, preferences.email_override, preferences.quiet_hours_enabled, preferences.quiet_hours_start, preferences.quiet_hours_end, user_id )) else: # Create new query = """ INSERT INTO user_notification_preferences ( user_id, notify_mattermost, notify_email, notify_frontend, email_override, quiet_hours_enabled, quiet_hours_start, quiet_hours_end ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """ execute_insert(query, ( user_id, preferences.notify_mattermost, preferences.notify_email, preferences.notify_frontend, preferences.email_override, preferences.quiet_hours_enabled, preferences.quiet_hours_start, preferences.quiet_hours_end )) logger.info(f"✅ Updated notification preferences for user {user_id}") return {"success": True, "message": "Preferences updated"} except Exception as e: logger.error(f"❌ Error updating preferences: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # Reminder CRUD Endpoints # ============================================================================ @router.get("/api/v1/sag/{sag_id}/reminders", response_model=List[ReminderResponse]) async def list_sag_reminders(sag_id: int): """List all reminders for a case""" query = """ SELECT id, sag_id, title, message, priority, trigger_type, recurrence_type, is_active, next_check_at, last_sent_at, created_at FROM sag_reminders WHERE sag_id = %s AND deleted_at IS NULL ORDER BY created_at DESC """ results = execute_query(query, (sag_id,)) return [ ReminderResponse( id=r['id'], sag_id=r['sag_id'], title=r['title'], message=r['message'], priority=r['priority'], trigger_type=r['trigger_type'], recurrence_type=r['recurrence_type'], is_active=r['is_active'], next_check_at=r['next_check_at'], last_sent_at=r['last_sent_at'], created_at=r['created_at'] ) for r in results ] @router.get("/api/v1/reminders/my", response_model=List[ReminderProfileResponse]) async def list_my_reminders(request: Request): """List reminders for the authenticated user""" user_id = _get_user_id_from_request(request) query = """ SELECT r.id, r.sag_id, r.title, r.message, r.priority, r.trigger_type, r.recurrence_type, r.is_active, r.next_check_at, r.last_sent_at, r.created_at, s.titel as case_title, c.name as customer_name FROM sag_reminders r LEFT JOIN sag_sager s ON s.id = r.sag_id LEFT JOIN customers c ON c.id = s.customer_id WHERE r.deleted_at IS NULL AND %s = ANY(r.recipient_user_ids) ORDER BY r.created_at DESC """ results = execute_query(query, (user_id,)) return [ ReminderProfileResponse( id=r['id'], sag_id=r['sag_id'], title=r['title'], message=r['message'], priority=r['priority'], trigger_type=r['trigger_type'], recurrence_type=r['recurrence_type'], is_active=r['is_active'], next_check_at=r['next_check_at'], last_sent_at=r['last_sent_at'], created_at=r['created_at'], case_title=r.get('case_title'), customer_name=r.get('customer_name') ) for r in results ] @router.post("/api/v1/sag/{sag_id}/reminders", response_model=ReminderResponse) async def create_sag_reminder(sag_id: int, request: Request, reminder: ReminderCreate): """Create a new reminder for a case""" user_id = _get_user_id_from_request(request) # Verify case exists case_query = "SELECT id FROM sag_sager WHERE id = %s" case = execute_query(case_query, (sag_id,)) if not case: raise HTTPException(status_code=404, detail=f"Case #{sag_id} not found") # Calculate next_check_at based on trigger type next_check_at = None if reminder.trigger_type == 'time_based' and reminder.scheduled_at: next_check_at = reminder.scheduled_at elif reminder.trigger_type == 'deadline_approaching': next_check_at = datetime.now() + timedelta(days=1) # Check daily try: import json query = """ INSERT INTO sag_reminders ( sag_id, title, message, priority, trigger_type, trigger_config, recipient_user_ids, recipient_emails, notify_mattermost, notify_email, notify_frontend, override_user_preferences, recurrence_type, recurrence_day_of_week, recurrence_day_of_month, scheduled_at, next_check_at, is_active, created_by_user_id ) VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, true, %s ) RETURNING id, sag_id, title, message, priority, trigger_type, recurrence_type, is_active, next_check_at, last_sent_at, created_at """ result = execute_insert(query, ( sag_id, reminder.title, reminder.message, reminder.priority, reminder.trigger_type, json.dumps(reminder.trigger_config), reminder.recipient_user_ids, reminder.recipient_emails, reminder.notify_mattermost, reminder.notify_email, reminder.notify_frontend, reminder.override_user_preferences, reminder.recurrence_type, reminder.recurrence_day_of_week, reminder.recurrence_day_of_month, reminder.scheduled_at, next_check_at, user_id )) if not result: raise HTTPException(status_code=500, detail="Failed to create reminder") raw_row = result[0] if isinstance(result, list) else result if isinstance(raw_row, dict): r = raw_row else: reminder_id = int(raw_row) fetch_query = """ SELECT id, sag_id, title, message, priority, trigger_type, recurrence_type, is_active, next_check_at, last_sent_at, created_at FROM sag_reminders WHERE id = %s """ fetched = execute_query(fetch_query, (reminder_id,)) if not fetched: raise HTTPException(status_code=500, detail="Failed to fetch reminder after creation") r = fetched[0] logger.info(f"✅ Reminder created for case #{sag_id} by user {user_id}") return ReminderResponse( id=r['id'], sag_id=r['sag_id'], title=r['title'], message=r['message'], priority=r['priority'], trigger_type=r['trigger_type'], recurrence_type=r['recurrence_type'], is_active=r['is_active'], next_check_at=r['next_check_at'], last_sent_at=r['last_sent_at'], created_at=r['created_at'] ) except Exception as e: logger.error(f"❌ Error creating reminder: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.patch("/api/v1/sag/reminders/{reminder_id}") async def update_sag_reminder(reminder_id: int, update: ReminderUpdate): """Update a reminder""" # Build update query dynamically updates = [] params = [] if update.title is not None: updates.append("title = %s") params.append(update.title) if update.message is not None: updates.append("message = %s") params.append(update.message) if update.priority is not None: updates.append("priority = %s") params.append(update.priority) if update.notify_mattermost is not None: updates.append("notify_mattermost = %s") params.append(update.notify_mattermost) if update.notify_email is not None: updates.append("notify_email = %s") params.append(update.notify_email) if update.notify_frontend is not None: updates.append("notify_frontend = %s") params.append(update.notify_frontend) if update.override_user_preferences is not None: updates.append("override_user_preferences = %s") params.append(update.override_user_preferences) if update.is_active is not None: updates.append("is_active = %s") params.append(update.is_active) if not updates: raise HTTPException(status_code=400, detail="No fields to update") updates.append("updated_at = CURRENT_TIMESTAMP") params.append(reminder_id) try: query = f""" UPDATE sag_reminders SET {', '.join(updates)} WHERE id = %s RETURNING id """ result = execute_insert(query, tuple(params)) if not result: raise HTTPException(status_code=404, detail="Reminder not found") logger.info(f"✅ Reminder {reminder_id} updated") return {"success": True, "message": "Reminder updated"} except Exception as e: logger.error(f"❌ Error updating reminder: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/api/v1/sag/reminders/{reminder_id}") async def delete_sag_reminder(reminder_id: int): """Soft-delete a reminder""" try: query = """ UPDATE sag_reminders SET deleted_at = CURRENT_TIMESTAMP, is_active = false WHERE id = %s RETURNING id """ result = execute_insert(query, (reminder_id,)) if not result: raise HTTPException(status_code=404, detail="Reminder not found") logger.info(f"✅ Reminder {reminder_id} deleted") return {"success": True, "message": "Reminder deleted"} except Exception as e: logger.error(f"❌ Error deleting reminder: {e}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # Reminder Interaction Endpoints # ============================================================================ @router.post("/api/v1/sag/reminders/{reminder_id}/snooze") async def snooze_reminder(reminder_id: int, request: Request, snooze_request: SnoozeRequest): """Snooze a reminder for specified minutes""" user_id = _get_user_id_from_request(request) snooze_until = datetime.now() + timedelta(minutes=snooze_request.duration_minutes) try: query = """ INSERT INTO sag_reminder_logs ( reminder_id, sag_id, user_id, status, snoozed_until, snoozed_by_user_id, triggered_at ) SELECT id, sag_id, %s, 'snoozed', %s, %s, CURRENT_TIMESTAMP FROM sag_reminders WHERE id = %s RETURNING id """ result = execute_insert(query, (user_id, snooze_until, user_id, reminder_id)) if not result: raise HTTPException(status_code=404, detail="Reminder not found") logger.info(f"✅ Reminder {reminder_id} snoozed until {snooze_until}") return { "success": True, "message": f"Reminder snoozed for {snooze_request.duration_minutes} minutes", "snoozed_until": snooze_until } except Exception as e: logger.error(f"❌ Error snoozing reminder: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/api/v1/sag/reminders/{reminder_id}/dismiss") async def dismiss_reminder(reminder_id: int, request: Request, dismiss_request: DismissRequest): """Dismiss a reminder""" user_id = _get_user_id_from_request(request) try: query = """ INSERT INTO sag_reminder_logs ( reminder_id, sag_id, user_id, status, dismissed_at, dismissed_by_user_id, triggered_at ) SELECT id, sag_id, %s, 'dismissed', CURRENT_TIMESTAMP, %s, CURRENT_TIMESTAMP FROM sag_reminders WHERE id = %s RETURNING id """ result = execute_insert(query, (user_id, user_id, reminder_id)) if not result: raise HTTPException(status_code=404, detail="Reminder not found") logger.info(f"✅ Reminder {reminder_id} dismissed by user {user_id}") return {"success": True, "message": "Reminder dismissed"} except Exception as e: logger.error(f"❌ Error dismissing reminder: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/api/v1/reminders/pending/me") async def get_pending_reminders(request: Request): """Get pending reminders for current user (for frontend polling)""" user_id = _get_user_id_from_request(request) query = """ SELECT r.id, r.sag_id, r.title, r.message, r.priority, s.titel as case_title, c.name as customer_name, l.id as log_id, l.snoozed_until, l.status as log_status FROM sag_reminders r JOIN sag_sager s ON r.sag_id = s.id JOIN customers c ON s.customer_id = c.id LEFT JOIN LATERAL ( SELECT id, snoozed_until, status, triggered_at FROM sag_reminder_logs WHERE reminder_id = r.id AND user_id = %s ORDER BY triggered_at DESC LIMIT 1 ) l ON true WHERE r.is_active = true AND r.deleted_at IS NULL AND r.next_check_at <= CURRENT_TIMESTAMP AND %s = ANY(r.recipient_user_ids) AND (l.snoozed_until IS NULL OR l.snoozed_until < CURRENT_TIMESTAMP) AND (l.status IS NULL OR l.status != 'dismissed') ORDER BY r.priority DESC, r.next_check_at ASC LIMIT 5 """ try: results = execute_query(query, (user_id, user_id)) return [{ 'id': r['id'], 'sag_id': r['sag_id'], 'title': r['title'], 'message': r['message'], 'priority': r['priority'], 'case_title': r['case_title'], 'customer_name': r['customer_name'] } for r in results] except Exception as e: logger.error(f"❌ Error fetching pending reminders: {e}") raise HTTPException(status_code=500, detail=str(e))