Files
bakery-ia/services/notification/app/repositories/template_repository.py
2025-08-08 09:08:41 +02:00

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 []