REFACTOR - Database logic

This commit is contained in:
Urtzi Alfaro
2025-08-08 09:08:41 +02:00
parent 0154365bfc
commit 488bb3ef93
113 changed files with 22842 additions and 6503 deletions

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