450 lines
19 KiB
Python
450 lines
19 KiB
Python
"""
|
|
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 [] |