import uuid from typing import List, Dict, Any, Optional from sqlalchemy.orm import Session from app.models import Trigger, TriggerLog, Task, User, Project from app.services.notification_service import NotificationService class TriggerService: """Service for evaluating and executing triggers.""" SUPPORTED_FIELDS = ["status_id", "assignee_id", "priority"] SUPPORTED_OPERATORS = ["equals", "not_equals", "changed_to", "changed_from"] @staticmethod def evaluate_triggers( db: Session, task: Task, old_values: Dict[str, Any], new_values: Dict[str, Any], current_user: User, ) -> List[TriggerLog]: """Evaluate all active triggers for a project when task values change.""" logs = [] # Get active field_change triggers for the project triggers = db.query(Trigger).filter( Trigger.project_id == task.project_id, Trigger.is_active == True, Trigger.trigger_type == "field_change", ).all() for trigger in triggers: if TriggerService._check_conditions(trigger.conditions, old_values, new_values): log = TriggerService._execute_actions(db, trigger, task, current_user, old_values, new_values) logs.append(log) return logs @staticmethod def _check_conditions( conditions: Dict[str, Any], old_values: Dict[str, Any], new_values: Dict[str, Any], ) -> bool: """Check if trigger conditions are met.""" field = conditions.get("field") operator = conditions.get("operator") value = conditions.get("value") if field not in TriggerService.SUPPORTED_FIELDS: return False old_value = old_values.get(field) new_value = new_values.get(field) if operator == "equals": return new_value == value elif operator == "not_equals": return new_value != value elif operator == "changed_to": return old_value != value and new_value == value elif operator == "changed_from": return old_value == value and new_value != value return False @staticmethod def _execute_actions( db: Session, trigger: Trigger, task: Task, current_user: User, old_values: Dict[str, Any], new_values: Dict[str, Any], ) -> TriggerLog: """Execute trigger actions and log the result.""" actions = trigger.actions if isinstance(trigger.actions, list) else [trigger.actions] executed_actions = [] error_message = None try: for action in actions: action_type = action.get("type") if action_type == "notify": TriggerService._execute_notify_action(db, action, task, current_user, old_values, new_values) executed_actions.append({"type": action_type, "status": "success"}) status = "success" except Exception as e: status = "failed" error_message = str(e) executed_actions.append({"type": "error", "message": str(e)}) log = TriggerLog( id=str(uuid.uuid4()), trigger_id=trigger.id, task_id=task.id, status=status, details={ "trigger_name": trigger.name, "old_values": old_values, "new_values": new_values, "actions_executed": executed_actions, }, error_message=error_message, ) db.add(log) return log @staticmethod def _execute_notify_action( db: Session, action: Dict[str, Any], task: Task, current_user: User, old_values: Dict[str, Any], new_values: Dict[str, Any], ) -> None: """Execute a notify action.""" target = action.get("target", "assignee") template = action.get("template", "任務 {task_title} 已觸發自動化規則") # Resolve target user target_user_id = TriggerService._resolve_target(task, target) if not target_user_id: return # Don't notify the user who triggered the action if target_user_id == current_user.id: return # Format message with variables message = TriggerService._format_template(template, task, old_values, new_values) NotificationService.create_notification( db=db, user_id=target_user_id, notification_type="status_change", reference_type="task", reference_id=task.id, title=f"自動化通知: {task.title}", message=message, ) @staticmethod def _resolve_target(task: Task, target: str) -> Optional[str]: """Resolve notification target to user ID.""" if target == "assignee": return task.assignee_id elif target == "creator": return task.created_by elif target == "project_owner": return task.project.owner_id if task.project else None elif target.startswith("user:"): return target.split(":", 1)[1] return None @staticmethod def _format_template( template: str, task: Task, old_values: Dict[str, Any], new_values: Dict[str, Any], ) -> str: """Format message template with task variables.""" replacements = { "{task_title}": task.title, "{task_id}": task.id, "{old_value}": str(old_values.get("status_id", old_values.get("assignee_id", old_values.get("priority", "")))), "{new_value}": str(new_values.get("status_id", new_values.get("assignee_id", new_values.get("priority", "")))), } result = template for key, value in replacements.items(): result = result.replace(key, value) return result @staticmethod def log_execution( db: Session, trigger: Trigger, task: Optional[Task], status: str, details: Optional[Dict[str, Any]] = None, error_message: Optional[str] = None, ) -> TriggerLog: """Log a trigger execution.""" log = TriggerLog( id=str(uuid.uuid4()), trigger_id=trigger.id, task_id=task.id if task else None, status=status, details=details, error_message=error_message, ) db.add(log) return log