""" Alert Notes Router API endpoints for contextual customer/contact alert system """ from fastapi import APIRouter, HTTPException, Depends, Query from typing import List, Optional, Dict import logging from datetime import datetime from app.core.database import execute_query, execute_update from app.core.auth_dependencies import require_permission, get_current_user from app.alert_notes.backend.schemas import ( AlertNoteCreate, AlertNoteUpdate, AlertNoteFull, AlertNoteCheck, AlertNoteRestriction, AlertNoteAcknowledgement, EntityType, Severity ) logger = logging.getLogger(__name__) router = APIRouter() def _check_user_can_handle(alert_id: int, current_user: dict) -> bool: """ Check if current user is allowed to handle the entity based on restrictions. Returns True if no restrictions exist OR user matches a restriction. """ # Superadmins bypass restrictions if current_user.get("is_superadmin"): return True # Get restrictions for this alert restrictions = execute_query( """ SELECT restriction_type, restriction_id FROM alert_note_restrictions WHERE alert_note_id = %s """, (alert_id,) ) # No restrictions = everyone can handle if not restrictions: return True user_id = current_user["id"] # Get user's group IDs user_groups = execute_query( "SELECT group_id FROM user_groups WHERE user_id = %s", (user_id,) ) user_group_ids = [g["group_id"] for g in user_groups] # Check if user matches any restriction for restriction in restrictions: if restriction["restriction_type"] == "user" and restriction["restriction_id"] == user_id: return True if restriction["restriction_type"] == "group" and restriction["restriction_id"] in user_group_ids: return True return False def _get_entity_name(entity_type: str, entity_id: int) -> Optional[str]: """Get the name of the entity (customer or contact)""" if entity_type == "customer": result = execute_query( "SELECT name FROM customers WHERE id = %s", (entity_id,) ) return result[0]["name"] if result else None elif entity_type == "contact": result = execute_query( "SELECT first_name, last_name FROM contacts WHERE id = %s", (entity_id,) ) if result: return f"{result[0]['first_name']} {result[0]['last_name']}" return None def _get_alert_with_relations(alert_id: int, current_user: dict) -> Optional[Dict]: """Get alert note with all its relations""" # Get main alert alerts = execute_query( """ SELECT an.*, u.full_name as created_by_user_name FROM alert_notes an LEFT JOIN users u ON an.created_by_user_id = u.user_id WHERE an.id = %s """, (alert_id,) ) if not alerts: return None alert = dict(alerts[0]) # Get entity name alert["entity_name"] = _get_entity_name(alert["entity_type"], alert["entity_id"]) # Get restrictions restrictions = execute_query( """ SELECT anr.*, CASE WHEN anr.restriction_type = 'group' THEN g.name WHEN anr.restriction_type = 'user' THEN u.full_name END as restriction_name FROM alert_note_restrictions anr LEFT JOIN groups g ON anr.restriction_type = 'group' AND anr.restriction_id = g.id LEFT JOIN users u ON anr.restriction_type = 'user' AND anr.restriction_id = u.user_id WHERE anr.alert_note_id = %s """, (alert_id,) ) alert["restrictions"] = restrictions # Get acknowledgements acknowledgements = execute_query( """ SELECT ana.*, u.full_name as user_name FROM alert_note_acknowledgements ana LEFT JOIN users u ON ana.user_id = u.user_id WHERE ana.alert_note_id = %s ORDER BY ana.acknowledged_at DESC """, (alert_id,) ) alert["acknowledgements"] = acknowledgements return alert @router.get("/alert-notes/check", response_model=AlertNoteCheck) async def check_alerts( entity_type: EntityType = Query(..., description="Entity type (customer/contact)"), entity_id: int = Query(..., description="Entity ID"), current_user: dict = Depends(get_current_user) ): """ Check if there are active alert notes for a specific entity. Returns alerts that the current user is allowed to see based on restrictions. """ # Get active alerts for this entity alerts = execute_query( """ SELECT an.*, u.full_name as created_by_user_name FROM alert_notes an LEFT JOIN users u ON an.created_by_user_id = u.user_id WHERE an.entity_type = %s AND an.entity_id = %s AND an.active = TRUE ORDER BY CASE an.severity WHEN 'critical' THEN 1 WHEN 'warning' THEN 2 WHEN 'info' THEN 3 END, an.created_at DESC """, (entity_type.value, entity_id) ) if not alerts: return AlertNoteCheck( has_alerts=False, alerts=[], user_can_handle=True, user_has_acknowledged=False ) # Enrich alerts with relations enriched_alerts = [] for alert in alerts: alert_dict = dict(alert) alert_dict["entity_name"] = _get_entity_name(alert["entity_type"], alert["entity_id"]) # Get restrictions restrictions = execute_query( """ SELECT anr.*, CASE WHEN anr.restriction_type = 'group' THEN g.name WHEN anr.restriction_type = 'user' THEN u.full_name END as restriction_name FROM alert_note_restrictions anr LEFT JOIN groups g ON anr.restriction_type = 'group' AND anr.restriction_id = g.id LEFT JOIN users u ON anr.restriction_type = 'user' AND anr.restriction_id = u.user_id WHERE anr.alert_note_id = %s """, (alert["id"],) ) alert_dict["restrictions"] = restrictions # Get acknowledgements acknowledgements = execute_query( """ SELECT ana.*, u.full_name as user_name FROM alert_note_acknowledgements ana LEFT JOIN users u ON ana.user_id = u.user_id WHERE ana.alert_note_id = %s ORDER BY ana.acknowledged_at DESC """, (alert["id"],) ) alert_dict["acknowledgements"] = acknowledgements enriched_alerts.append(alert_dict) # Check if user can handle based on restrictions user_can_handle = all(_check_user_can_handle(a["id"], current_user) for a in alerts) # Check if user has acknowledged all alerts that require it user_id = current_user["id"] user_has_acknowledged = True for alert in alerts: if alert["requires_acknowledgement"]: ack = execute_query( "SELECT id FROM alert_note_acknowledgements WHERE alert_note_id = %s AND user_id = %s", (alert["id"], user_id) ) if not ack: user_has_acknowledged = False break return AlertNoteCheck( has_alerts=True, alerts=enriched_alerts, user_can_handle=user_can_handle, user_has_acknowledged=user_has_acknowledged ) @router.post("/alert-notes/{alert_id}/acknowledge") async def acknowledge_alert( alert_id: int, current_user: dict = Depends(get_current_user) ): """ Mark an alert note as acknowledged by the current user. """ # Check if alert exists alert = execute_query( "SELECT id, active FROM alert_notes WHERE id = %s", (alert_id,) ) if not alert: raise HTTPException(status_code=404, detail="Alert note not found") if not alert[0]["active"]: raise HTTPException(status_code=400, detail="Alert note is not active") user_id = current_user["id"] # Check if already acknowledged existing = execute_query( "SELECT id FROM alert_note_acknowledgements WHERE alert_note_id = %s AND user_id = %s", (alert_id, user_id) ) if existing: return {"status": "already_acknowledged", "alert_id": alert_id} # Insert acknowledgement execute_update( """ INSERT INTO alert_note_acknowledgements (alert_note_id, user_id) VALUES (%s, %s) """, (alert_id, user_id) ) logger.info(f"Alert {alert_id} acknowledged by user {user_id}") return {"status": "acknowledged", "alert_id": alert_id} @router.get("/alert-notes", response_model=List[AlertNoteFull]) async def list_alerts( entity_type: Optional[EntityType] = Query(None), entity_id: Optional[int] = Query(None), severity: Optional[Severity] = Query(None), active: Optional[bool] = Query(None), limit: int = Query(default=50, ge=1, le=500), offset: int = Query(default=0, ge=0), current_user: dict = Depends(require_permission("alert_notes.view")) ): """ List alert notes with filtering (admin endpoint). Requires alert_notes.view permission. """ conditions = [] params = [] if entity_type: conditions.append("an.entity_type = %s") params.append(entity_type.value) if entity_id: conditions.append("an.entity_id = %s") params.append(entity_id) if severity: conditions.append("an.severity = %s") params.append(severity.value) if active is not None: conditions.append("an.active = %s") params.append(active) where_clause = "WHERE " + " AND ".join(conditions) if conditions else "" query = f""" SELECT an.*, u.full_name as created_by_user_name FROM alert_notes an LEFT JOIN users u ON an.created_by_user_id = u.user_id {where_clause} ORDER BY an.created_at DESC LIMIT %s OFFSET %s """ params.extend([limit, offset]) alerts = execute_query(query, tuple(params)) # Enrich with relations enriched_alerts = [] for alert in alerts: alert_full = _get_alert_with_relations(alert["id"], current_user) if alert_full: enriched_alerts.append(alert_full) return enriched_alerts @router.post("/alert-notes", response_model=AlertNoteFull) async def create_alert( alert: AlertNoteCreate, current_user: dict = Depends(require_permission("alert_notes.create")) ): """ Create a new alert note. Requires alert_notes.create permission. """ # Verify entity exists entity_name = _get_entity_name(alert.entity_type.value, alert.entity_id) if not entity_name: raise HTTPException( status_code=404, detail=f"{alert.entity_type.value.capitalize()} with ID {alert.entity_id} not found" ) # Insert alert note result = execute_query( """ INSERT INTO alert_notes ( entity_type, entity_id, title, message, severity, requires_acknowledgement, active, created_by_user_id ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( alert.entity_type.value, alert.entity_id, alert.title, alert.message, alert.severity.value, alert.requires_acknowledgement, alert.active, current_user["id"] ) ) if not result or len(result) == 0: raise HTTPException(status_code=500, detail="Failed to create alert note") alert_id = result[0]["id"] # Insert restrictions for group_id in alert.restriction_group_ids: execute_query( """ INSERT INTO alert_note_restrictions (alert_note_id, restriction_type, restriction_id) VALUES (%s, 'group', %s) """, (alert_id, group_id) ) for user_id in alert.restriction_user_ids: execute_query( """ INSERT INTO alert_note_restrictions (alert_note_id, restriction_type, restriction_id) VALUES (%s, 'user', %s) """, (alert_id, user_id) ) logger.info(f"Alert note {alert_id} created for {alert.entity_type.value} {alert.entity_id} by user {current_user['id']}") # Return full alert with relations alert_full = _get_alert_with_relations(alert_id, current_user) return alert_full @router.patch("/alert-notes/{alert_id}", response_model=AlertNoteFull) async def update_alert( alert_id: int, alert_update: AlertNoteUpdate, current_user: dict = Depends(require_permission("alert_notes.edit")) ): """ Update an existing alert note. Requires alert_notes.edit permission. """ # Check if alert exists existing = execute_query( "SELECT id FROM alert_notes WHERE id = %s", (alert_id,) ) if not existing: raise HTTPException(status_code=404, detail="Alert note not found") # Build update query update_fields = [] params = [] if alert_update.title is not None: update_fields.append("title = %s") params.append(alert_update.title) if alert_update.message is not None: update_fields.append("message = %s") params.append(alert_update.message) if alert_update.severity is not None: update_fields.append("severity = %s") params.append(alert_update.severity.value) if alert_update.requires_acknowledgement is not None: update_fields.append("requires_acknowledgement = %s") params.append(alert_update.requires_acknowledgement) if alert_update.active is not None: update_fields.append("active = %s") params.append(alert_update.active) if update_fields: query = f"UPDATE alert_notes SET {', '.join(update_fields)} WHERE id = %s" params.append(alert_id) execute_update(query, tuple(params)) # Update restrictions if provided if alert_update.restriction_group_ids is not None or alert_update.restriction_user_ids is not None: # Delete existing restrictions execute_update( "DELETE FROM alert_note_restrictions WHERE alert_note_id = %s", (alert_id,) ) # Insert new group restrictions if alert_update.restriction_group_ids is not None: for group_id in alert_update.restriction_group_ids: execute_update( """ INSERT INTO alert_note_restrictions (alert_note_id, restriction_type, restriction_id) VALUES (%s, 'group', %s) """, (alert_id, group_id) ) # Insert new user restrictions if alert_update.restriction_user_ids is not None: for user_id in alert_update.restriction_user_ids: execute_update( """ INSERT INTO alert_note_restrictions (alert_note_id, restriction_type, restriction_id) VALUES (%s, 'user', %s) """, (alert_id, user_id) ) logger.info(f"Alert note {alert_id} updated by user {current_user['id']}") # Return updated alert alert_full = _get_alert_with_relations(alert_id, current_user) return alert_full @router.delete("/alert-notes/{alert_id}") async def delete_alert( alert_id: int, current_user: dict = Depends(require_permission("alert_notes.delete")) ): """ Soft delete an alert note (sets active = false). Requires alert_notes.delete permission. """ # Check if alert exists existing = execute_query( "SELECT id, active FROM alert_notes WHERE id = %s", (alert_id,) ) if not existing: raise HTTPException(status_code=404, detail="Alert note not found") # Soft delete execute_update( "UPDATE alert_notes SET active = FALSE WHERE id = %s", (alert_id,) ) logger.info(f"Alert note {alert_id} deactivated by user {current_user['id']}") return {"status": "deleted", "alert_id": alert_id}