""" Template Repository Repository for notification template operations """ from typing import Optional, List, Dict, Any from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, text, and_ from datetime import datetime import structlog import json from .base import NotificationBaseRepository from app.models.notifications import NotificationTemplate, NotificationType from shared.database.exceptions import DatabaseError, ValidationError, DuplicateRecordError logger = structlog.get_logger() class TemplateRepository(NotificationBaseRepository): """Repository for notification template operations""" def __init__(self, session: AsyncSession, cache_ttl: Optional[int] = 1800): # Templates don't change often, longer cache time (30 minutes) super().__init__(NotificationTemplate, session, cache_ttl) async def create_template(self, template_data: Dict[str, Any]) -> NotificationTemplate: """Create a new notification template with validation""" try: # Validate template data required_fields = ["template_key", "name", "category", "type", "body_template"] validation_result = self._validate_notification_data(template_data, required_fields) # Additional template-specific validation if validation_result["is_valid"]: # Check if template_key already exists existing_template = await self.get_by_template_key(template_data["template_key"]) if existing_template: raise DuplicateRecordError(f"Template key {template_data['template_key']} already exists") # Validate template variables if provided if "required_variables" in template_data: if isinstance(template_data["required_variables"], list): template_data["required_variables"] = json.dumps(template_data["required_variables"]) elif isinstance(template_data["required_variables"], str): # Verify it's valid JSON try: json.loads(template_data["required_variables"]) except json.JSONDecodeError: validation_result["errors"].append("Invalid JSON format in required_variables") validation_result["is_valid"] = False if not validation_result["is_valid"]: raise ValidationError(f"Invalid template data: {validation_result['errors']}") # Set default values if "language" not in template_data: template_data["language"] = "es" if "is_active" not in template_data: template_data["is_active"] = True if "is_system" not in template_data: template_data["is_system"] = False if "default_priority" not in template_data: template_data["default_priority"] = "normal" # Create template template = await self.create(template_data) logger.info("Notification template created successfully", template_id=template.id, template_key=template.template_key, type=template.type.value, category=template.category) return template except (ValidationError, DuplicateRecordError): raise except Exception as e: logger.error("Failed to create template", template_key=template_data.get("template_key"), error=str(e)) raise DatabaseError(f"Failed to create template: {str(e)}") async def get_by_template_key(self, template_key: str) -> Optional[NotificationTemplate]: """Get template by template key""" try: return await self.get_by_field("template_key", template_key) except Exception as e: logger.error("Failed to get template by key", template_key=template_key, error=str(e)) raise DatabaseError(f"Failed to get template: {str(e)}") async def get_templates_by_category( self, category: str, tenant_id: str = None, include_system: bool = True ) -> List[NotificationTemplate]: """Get templates by category""" try: filters = {"category": category, "is_active": True} if tenant_id and include_system: # Get both tenant-specific and system templates tenant_templates = await self.get_multi( filters={**filters, "tenant_id": tenant_id} ) system_templates = await self.get_multi( filters={**filters, "is_system": True} ) return tenant_templates + system_templates elif tenant_id: # Only tenant-specific templates filters["tenant_id"] = tenant_id return await self.get_multi(filters=filters) elif include_system: # Only system templates filters["is_system"] = True return await self.get_multi(filters=filters) else: return [] except Exception as e: logger.error("Failed to get templates by category", category=category, tenant_id=tenant_id, error=str(e)) return [] async def get_templates_by_type( self, notification_type: NotificationType, tenant_id: str = None, include_system: bool = True ) -> List[NotificationTemplate]: """Get templates by notification type""" try: filters = {"type": notification_type, "is_active": True} if tenant_id and include_system: # Get both tenant-specific and system templates tenant_templates = await self.get_multi( filters={**filters, "tenant_id": tenant_id} ) system_templates = await self.get_multi( filters={**filters, "is_system": True} ) return tenant_templates + system_templates elif tenant_id: # Only tenant-specific templates filters["tenant_id"] = tenant_id return await self.get_multi(filters=filters) elif include_system: # Only system templates filters["is_system"] = True return await self.get_multi(filters=filters) else: return [] except Exception as e: logger.error("Failed to get templates by type", notification_type=notification_type.value, tenant_id=tenant_id, error=str(e)) return [] async def update_template( self, template_id: str, update_data: Dict[str, Any], allow_system_update: bool = False ) -> Optional[NotificationTemplate]: """Update template with system template protection""" try: template = await self.get_by_id(template_id) if not template: return None # Prevent updating system templates unless explicitly allowed if template.is_system and not allow_system_update: raise ValidationError("Cannot update system templates") # Validate required_variables if being updated if "required_variables" in update_data: if isinstance(update_data["required_variables"], list): update_data["required_variables"] = json.dumps(update_data["required_variables"]) elif isinstance(update_data["required_variables"], str): try: json.loads(update_data["required_variables"]) except json.JSONDecodeError: raise ValidationError("Invalid JSON format in required_variables") # Update template updated_template = await self.update(template_id, update_data) logger.info("Template updated successfully", template_id=template_id, template_key=template.template_key, updated_fields=list(update_data.keys())) return updated_template except ValidationError: raise except Exception as e: logger.error("Failed to update template", template_id=template_id, error=str(e)) raise DatabaseError(f"Failed to update template: {str(e)}") async def deactivate_template(self, template_id: str) -> Optional[NotificationTemplate]: """Deactivate a template (soft delete)""" try: template = await self.get_by_id(template_id) if not template: return None # Prevent deactivating system templates if template.is_system: raise ValidationError("Cannot deactivate system templates") updated_template = await self.update(template_id, { "is_active": False, "updated_at": datetime.utcnow() }) logger.info("Template deactivated", template_id=template_id, template_key=template.template_key) return updated_template except ValidationError: raise except Exception as e: logger.error("Failed to deactivate template", template_id=template_id, error=str(e)) raise DatabaseError(f"Failed to deactivate template: {str(e)}") async def activate_template(self, template_id: str) -> Optional[NotificationTemplate]: """Activate a template""" try: updated_template = await self.update(template_id, { "is_active": True, "updated_at": datetime.utcnow() }) if updated_template: logger.info("Template activated", template_id=template_id, template_key=updated_template.template_key) return updated_template except Exception as e: logger.error("Failed to activate template", template_id=template_id, error=str(e)) raise DatabaseError(f"Failed to activate template: {str(e)}") async def search_templates( self, search_term: str, tenant_id: str = None, category: str = None, notification_type: NotificationType = None, include_system: bool = True, limit: int = 50 ) -> List[NotificationTemplate]: """Search templates by name, description, or template key""" try: conditions = [ "is_active = true", "(LOWER(name) LIKE LOWER(:search_term) OR LOWER(description) LIKE LOWER(:search_term) OR LOWER(template_key) LIKE LOWER(:search_term))" ] params = {"search_term": f"%{search_term}%", "limit": limit} # Add tenant/system filter if tenant_id and include_system: conditions.append("(tenant_id = :tenant_id OR is_system = true)") params["tenant_id"] = tenant_id elif tenant_id: conditions.append("tenant_id = :tenant_id") params["tenant_id"] = tenant_id elif include_system: conditions.append("is_system = true") # Add category filter if category: conditions.append("category = :category") params["category"] = category # Add type filter if notification_type: conditions.append("type = :notification_type") params["notification_type"] = notification_type.value query_text = f""" SELECT * FROM notification_templates WHERE {' AND '.join(conditions)} ORDER BY name ASC LIMIT :limit """ result = await self.session.execute(text(query_text), params) templates = [] for row in result.fetchall(): record_dict = dict(row._mapping) # Convert enum string back to enum object record_dict["type"] = NotificationType(record_dict["type"]) template = self.model(**record_dict) templates.append(template) return templates except Exception as e: logger.error("Failed to search templates", search_term=search_term, error=str(e)) return [] async def get_template_usage_statistics(self, template_id: str) -> Dict[str, Any]: """Get usage statistics for a template""" try: template = await self.get_by_id(template_id) if not template: return {"error": "Template not found"} # Get usage statistics from notifications table usage_query = text(""" SELECT COUNT(*) as total_uses, COUNT(CASE WHEN status = 'delivered' THEN 1 END) as successful_uses, COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_uses, COUNT(CASE WHEN created_at >= NOW() - INTERVAL '30 days' THEN 1 END) as uses_last_30_days, MIN(created_at) as first_used, MAX(created_at) as last_used FROM notifications WHERE template_id = :template_key """) result = await self.session.execute(usage_query, {"template_key": template.template_key}) stats = result.fetchone() total = stats.total_uses or 0 successful = stats.successful_uses or 0 success_rate = (successful / total * 100) if total > 0 else 0 return { "template_id": template_id, "template_key": template.template_key, "total_uses": total, "successful_uses": successful, "failed_uses": stats.failed_uses or 0, "success_rate_percent": round(success_rate, 2), "uses_last_30_days": stats.uses_last_30_days or 0, "first_used": stats.first_used.isoformat() if stats.first_used else None, "last_used": stats.last_used.isoformat() if stats.last_used else None } except Exception as e: logger.error("Failed to get template usage statistics", template_id=template_id, error=str(e)) return { "template_id": template_id, "error": str(e) } async def duplicate_template( self, template_id: str, new_template_key: str, new_name: str, tenant_id: str = None ) -> Optional[NotificationTemplate]: """Duplicate an existing template""" try: original_template = await self.get_by_id(template_id) if not original_template: return None # Check if new template key already exists existing_template = await self.get_by_template_key(new_template_key) if existing_template: raise DuplicateRecordError(f"Template key {new_template_key} already exists") # Create duplicate template data duplicate_data = { "template_key": new_template_key, "name": new_name, "description": f"Copy of {original_template.name}", "category": original_template.category, "type": original_template.type, "subject_template": original_template.subject_template, "body_template": original_template.body_template, "html_template": original_template.html_template, "language": original_template.language, "default_priority": original_template.default_priority, "required_variables": original_template.required_variables, "tenant_id": tenant_id, "is_active": True, "is_system": False # Duplicates are never system templates } duplicated_template = await self.create(duplicate_data) logger.info("Template duplicated successfully", original_template_id=template_id, new_template_id=duplicated_template.id, new_template_key=new_template_key) return duplicated_template except DuplicateRecordError: raise except Exception as e: logger.error("Failed to duplicate template", template_id=template_id, new_template_key=new_template_key, error=str(e)) raise DatabaseError(f"Failed to duplicate template: {str(e)}") async def get_system_templates(self) -> List[NotificationTemplate]: """Get all system templates""" try: return await self.get_multi( filters={"is_system": True, "is_active": True}, order_by="category" ) except Exception as e: logger.error("Failed to get system templates", error=str(e)) return [] async def get_tenant_templates(self, tenant_id: str) -> List[NotificationTemplate]: """Get all templates for a specific tenant""" try: return await self.get_multi( filters={"tenant_id": tenant_id, "is_active": True}, order_by="category" ) except Exception as e: logger.error("Failed to get tenant templates", tenant_id=tenant_id, error=str(e)) return []