REFACTOR - Database logic
This commit is contained in:
450
services/notification/app/repositories/template_repository.py
Normal file
450
services/notification/app/repositories/template_repository.py
Normal file
@@ -0,0 +1,450 @@
|
||||
"""
|
||||
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 []
|
||||
Reference in New Issue
Block a user