Initial commit

This commit is contained in:
2026-01-12 08:17:17 -07:00
commit de5b6feef4
104 changed files with 12925 additions and 0 deletions

View File

@@ -0,0 +1,404 @@
"""
Email Tasks - ServiceManagerWeb Workers
Tareas asíncronas para envío de emails y notificaciones
"""
from celery import current_task
from celery.exceptions import Retry
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import smtplib
import ssl
from typing import Dict, List, Optional, Any
from jinja2 import Template, Environment, BaseLoader
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
settings = get_settings()
logger = get_logger(__name__)
class MemoryLoader(BaseLoader):
"""Jinja2 loader for templates from memory/database."""
def __init__(self, templates: Dict[str, str]):
self.templates = templates
def get_source(self, environment, template):
if template in self.templates:
source = self.templates[template]
return source, None, lambda: True
raise TemplateNotFoundError(template)
@celery_app.task(
bind=True,
autoretry_for=(Exception,),
retry_kwargs={'max_retries': 3, 'countdown': 60},
time_limit=120,
soft_time_limit=90
)
def send_email_task(
self,
to_email: str,
subject: str,
html_content: str,
text_content: Optional[str] = None,
from_email: Optional[str] = None,
from_name: Optional[str] = None,
attachments: Optional[List[Dict[str, Any]]] = None,
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send email using SMTP.
Args:
to_email: Recipient email address
subject: Email subject
html_content: HTML content
text_content: Plain text content (optional)
from_email: Sender email (optional, uses default)
from_name: Sender name (optional)
attachments: List of attachment dicts
tenant_id: Tenant ID for logging
correlation_id: Correlation ID for tracking
Returns:
Dict with send result
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info(
"Starting email send task",
to_email=to_email,
subject=subject
)
try:
# Prepare email
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f"{from_name or settings.DEFAULT_FROM_NAME} <{from_email or settings.DEFAULT_FROM_EMAIL}>"
msg['To'] = to_email
# Add text content
if text_content:
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
# Add HTML content
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# Add attachments
if attachments:
for attachment in attachments:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment['content'])
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename= {attachment["filename"]}'
)
msg.attach(part)
# Send email
context = ssl.create_default_context()
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
if settings.SMTP_USE_TLS:
server.starttls(context=context)
if settings.SMTP_USER and settings.SMTP_PASSWORD:
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.send_message(msg)
task_logger.info(
"Email sent successfully",
to_email=to_email,
subject=subject
)
return {
"success": True,
"to_email": to_email,
"subject": subject,
"sent_at": current_task.request.eta or "now"
}
except Exception as exc:
task_logger.error(
"Failed to send email",
to_email=to_email,
subject=subject,
error=str(exc),
exc_info=True
)
# Check if we should retry
if self.request.retries < self.max_retries:
task_logger.info(
"Retrying email send",
retry_count=self.request.retries + 1,
max_retries=self.max_retries
)
raise self.retry(countdown=60 * (2 ** self.request.retries))
return {
"success": False,
"to_email": to_email,
"subject": subject,
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_templated_email_task(
self,
to_email: str,
template_name: str,
context: Dict[str, Any],
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send email using a template.
Args:
to_email: Recipient email
template_name: Template identifier
context: Template context variables
tenant_id: Tenant ID
correlation_id: Correlation ID
Returns:
Dict with send result
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info(
"Starting templated email task",
to_email=to_email,
template_name=template_name
)
try:
# TODO: Fetch template from database
# For now, use mock templates
templates = {
"ticket_created": {
"subject": "Nuevo ticket #{{ ticket_number }}: {{ subject }}",
"html": """
<h2>Nuevo ticket creado</h2>
<p>Hola {{ user_name }},</p>
<p>Se ha creado un nuevo ticket:</p>
<ul>
<li><strong>Número:</strong> #{{ ticket_number }}</li>
<li><strong>Asunto:</strong> {{ subject }}</li>
<li><strong>Prioridad:</strong> {{ priority }}</li>
</ul>
<p><a href="{{ ticket_url }}">Ver ticket</a></p>
<p>Saludos,<br>Equipo de Soporte</p>
""",
"text": """
Nuevo ticket creado
Hola {{ user_name }},
Se ha creado un nuevo ticket:
Número: #{{ ticket_number }}
Asunto: {{ subject }}
Prioridad: {{ priority }}
Ver ticket: {{ ticket_url }}
Saludos,
Equipo de Soporte
"""
},
"ticket_assigned": {
"subject": "Ticket #{{ ticket_number }} asignado a ti",
"html": """
<h2>Ticket asignado</h2>
<p>Hola {{ agent_name }},</p>
<p>Se te ha asignado el ticket:</p>
<ul>
<li><strong>Número:</strong> #{{ ticket_number }}</li>
<li><strong>Asunto:</strong> {{ subject }}</li>
<li><strong>Cliente:</strong> {{ customer_name }}</li>
<li><strong>Prioridad:</strong> {{ priority }}</li>
</ul>
<p><a href="{{ ticket_url }}">Ver ticket</a></p>
""",
"text": """
Ticket asignado
Hola {{ agent_name }},
Se te ha asignado el ticket:
Número: #{{ ticket_number }}
Asunto: {{ subject }}
Cliente: {{ customer_name }}
Prioridad: {{ priority }}
Ver ticket: {{ ticket_url }}
"""
}
}
if template_name not in templates:
raise ValueError(f"Template '{template_name}' not found")
template_data = templates[template_name]
# Render templates
env = Environment(loader=MemoryLoader({
f"{template_name}_subject": template_data["subject"],
f"{template_name}_html": template_data["html"],
f"{template_name}_text": template_data["text"]
}))
subject_template = env.get_template(f"{template_name}_subject")
html_template = env.get_template(f"{template_name}_html")
text_template = env.get_template(f"{template_name}_text")
subject = subject_template.render(**context)
html_content = html_template.render(**context)
text_content = text_template.render(**context)
# Send email using the basic send task
return send_email_task.apply_async(
kwargs={
"to_email": to_email,
"subject": subject,
"html_content": html_content,
"text_content": text_content,
"tenant_id": tenant_id,
"correlation_id": correlation_id
}
).get()
except Exception as exc:
task_logger.error(
"Failed to send templated email",
to_email=to_email,
template_name=template_name,
error=str(exc),
exc_info=True
)
return {
"success": False,
"to_email": to_email,
"template_name": template_name,
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def send_bulk_email_task(
self,
email_list: List[Dict[str, Any]],
tenant_id: Optional[str] = None,
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send bulk emails.
Args:
email_list: List of email dicts with to_email, subject, content
tenant_id: Tenant ID
correlation_id: Correlation ID
Returns:
Dict with bulk send results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id,
correlation_id=correlation_id
)
total_emails = len(email_list)
task_logger.info(f"Starting bulk email task", total_emails=total_emails)
results = []
for i, email_data in enumerate(email_list):
try:
result = send_email_task.apply_async(
kwargs={
**email_data,
"tenant_id": tenant_id,
"correlation_id": correlation_id
}
).get()
results.append(result)
# Update task progress
current_task.update_state(
state='PROGRESS',
meta={'current': i + 1, 'total': total_emails}
)
except Exception as exc:
task_logger.error(
"Failed to send bulk email item",
index=i,
email_data=email_data,
error=str(exc)
)
results.append({
"success": False,
"to_email": email_data.get("to_email"),
"error": str(exc)
})
# Calculate stats
successful = sum(1 for r in results if r.get("success"))
failed = total_emails - successful
task_logger.info(
"Bulk email task completed",
total=total_emails,
successful=successful,
failed=failed
)
return {
"total": total_emails,
"successful": successful,
"failed": failed,
"results": results
}

View File

@@ -0,0 +1,371 @@
"""
Maintenance Tasks - ServiceManagerWeb Workers
Tareas de mantenimiento del sistema
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List
import os
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(bind=True)
def health_check(self) -> Dict[str, Any]:
"""
Worker health check task.
Returns basic health information about the worker.
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
try:
current_time = datetime.utcnow()
# Basic health checks
health_data = {
"status": "healthy",
"timestamp": current_time.isoformat(),
"worker_id": self.request.hostname,
"task_id": self.request.id,
"environment": settings.ENVIRONMENT,
"checks": {
"redis": "unknown", # TODO: Check Redis connectivity
"database": "unknown", # TODO: Check database connectivity
"disk_space": "unknown", # TODO: Check disk space
"memory": "unknown" # TODO: Check memory usage
}
}
task_logger.info("Worker health check completed", status="healthy")
return health_data
except Exception as exc:
task_logger.error(
"Worker health check failed",
error=str(exc),
exc_info=True
)
return {
"status": "unhealthy",
"timestamp": datetime.utcnow().isoformat(),
"error": str(exc)
}
@celery_app.task(
bind=True,
time_limit=1800, # 30 minutes
soft_time_limit=1500 # 25 minutes
)
def cleanup_old_logs(self) -> Dict[str, Any]:
"""
Clean up old log files and database records.
Removes:
- Log files older than LOG_RETENTION_DAYS
- Old notification logs
- Old audit logs (if configured)
- Temp files
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting cleanup of old logs")
try:
current_time = datetime.utcnow()
cutoff_date = current_time - timedelta(days=settings.LOG_RETENTION_DAYS)
cleanup_results = {
"started_at": current_time.isoformat(),
"cutoff_date": cutoff_date.isoformat(),
"files_removed": 0,
"bytes_freed": 0,
"database_records_removed": 0,
"errors": []
}
# TODO: Implement actual file cleanup
# For now, simulate cleanup
# Clean up log files
log_dir = "/app/logs"
if os.path.exists(log_dir):
for filename in os.listdir(log_dir):
filepath = os.path.join(log_dir, filename)
if os.path.isfile(filepath):
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
if file_mtime < cutoff_date and filename.endswith('.log'):
try:
file_size = os.path.getsize(filepath)
os.remove(filepath)
cleanup_results["files_removed"] += 1
cleanup_results["bytes_freed"] += file_size
task_logger.info(f"Removed old log file", filename=filename)
except Exception as e:
cleanup_results["errors"].append(f"Failed to remove {filename}: {str(e)}")
# TODO: Clean up database records
# - Old notification_logs
# - Old audit_logs (with retention policy)
# - Expired refresh_tokens
# - Old file attachments (if configured)
task_logger.info(
"Cleanup completed",
files_removed=cleanup_results["files_removed"],
bytes_freed=cleanup_results["bytes_freed"],
errors=len(cleanup_results["errors"])
)
return cleanup_results
except Exception as exc:
task_logger.error(
"Cleanup task failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=3600, # 1 hour
soft_time_limit=3300 # 55 minutes
)
def generate_weekly_reports(self) -> Dict[str, Any]:
"""
Generate weekly reports for all tenants.
Creates:
- SLA performance reports
- Ticket volume reports
- Agent performance reports
- Customer satisfaction reports
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting weekly reports generation")
try:
current_time = datetime.utcnow()
week_start = current_time - timedelta(days=7)
report_results = {
"generated_at": current_time.isoformat(),
"period_start": week_start.isoformat(),
"period_end": current_time.isoformat(),
"reports_generated": [],
"errors": []
}
# TODO: Get list of active tenants from database
mock_tenants = [
{"id": "tenant-1", "name": "Aduanasoft Demo", "slug": "aduanasoft-demo"}
]
for tenant in mock_tenants:
try:
task_logger.info(
"Generating report for tenant",
tenant_id=tenant["id"],
tenant_name=tenant["name"]
)
# TODO: Generate actual reports
# For now, simulate report generation
report_data = {
"tenant_id": tenant["id"],
"tenant_name": tenant["name"],
"period_start": week_start.isoformat(),
"period_end": current_time.isoformat(),
"metrics": {
"tickets_created": 25,
"tickets_resolved": 23,
"avg_response_time_hours": 2.1,
"avg_resolution_time_hours": 18.5,
"sla_response_met_percentage": 92.0,
"sla_resolution_met_percentage": 87.0,
"customer_satisfaction_avg": 4.2
}
}
report_results["reports_generated"].append(report_data)
# TODO: Store report in database
# TODO: Send report email to admins
# Update task progress
current_task.update_state(
state='PROGRESS',
meta={
'current': len(report_results["reports_generated"]),
'total': len(mock_tenants)
}
)
except Exception as e:
error_msg = f"Failed to generate report for tenant {tenant['id']}: {str(e)}"
report_results["errors"].append(error_msg)
task_logger.error(
"Report generation failed for tenant",
tenant_id=tenant["id"],
error=str(e)
)
task_logger.info(
"Weekly reports generation completed",
reports_generated=len(report_results["reports_generated"]),
errors=len(report_results["errors"])
)
return report_results
except Exception as exc:
task_logger.error(
"Weekly reports generation failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=900, # 15 minutes
soft_time_limit=780 # 13 minutes
)
def cleanup_temp_files(self) -> Dict[str, Any]:
"""
Clean up temporary files and orphaned uploads.
Removes:
- Temp upload files older than 24 hours
- Orphaned attachment files (no DB reference)
- Processing artifacts
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting temp files cleanup")
try:
current_time = datetime.utcnow()
cutoff_date = current_time - timedelta(hours=24)
cleanup_results = {
"started_at": current_time.isoformat(),
"temp_files_removed": 0,
"orphaned_files_removed": 0,
"bytes_freed": 0,
"errors": []
}
# Clean up temp directory
temp_dirs = ["/tmp", "/app/temp", f"{settings.UPLOAD_PATH}/temp"]
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
for filename in os.listdir(temp_dir):
filepath = os.path.join(temp_dir, filename)
if os.path.isfile(filepath):
try:
file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
if file_mtime < cutoff_date:
file_size = os.path.getsize(filepath)
os.remove(filepath)
cleanup_results["temp_files_removed"] += 1
cleanup_results["bytes_freed"] += file_size
except Exception as e:
cleanup_results["errors"].append(f"Failed to remove temp file {filepath}: {str(e)}")
# TODO: Check for orphaned files in uploads directory
# - Query database for all attachment file_paths
# - Compare with actual files in upload directory
# - Remove orphaned files
task_logger.info(
"Temp files cleanup completed",
temp_files_removed=cleanup_results["temp_files_removed"],
orphaned_files_removed=cleanup_results["orphaned_files_removed"],
bytes_freed=cleanup_results["bytes_freed"]
)
return cleanup_results
except Exception as exc:
task_logger.error(
"Temp files cleanup failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def database_maintenance(self) -> Dict[str, Any]:
"""
Perform database maintenance tasks.
- VACUUM and ANALYZE tables
- Update statistics
- Check for slow queries
- Optimize indices if needed
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting database maintenance")
try:
# TODO: Implement database maintenance
# For now, return placeholder results
maintenance_results = {
"started_at": datetime.utcnow().isoformat(),
"tables_analyzed": 0,
"indices_optimized": 0,
"slow_queries_found": 0,
"space_reclaimed_mb": 0
}
task_logger.info("Database maintenance completed (placeholder)")
return maintenance_results
except Exception as exc:
task_logger.error(
"Database maintenance failed",
error=str(exc),
exc_info=True
)
raise

View File

@@ -0,0 +1,469 @@
"""
Notification Tasks - ServiceManagerWeb Workers
Tareas para notificaciones y comunicaciones
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task, send_bulk_email_task
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(
bind=True,
time_limit=900, # 15 minutes
soft_time_limit=780 # 13 minutes
)
def send_daily_digest(self) -> Dict[str, Any]:
"""
Send daily digest emails to agents and managers.
Includes:
- New tickets assigned
- SLA warnings
- Performance summary
- Pending tasks
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting daily digest generation")
if not settings.DIGEST_ENABLED:
task_logger.info("Daily digest disabled, skipping")
return {"status": "disabled"}
try:
current_time = datetime.utcnow()
yesterday = current_time - timedelta(days=1)
digest_results = {
"generated_at": current_time.isoformat(),
"period_start": yesterday.isoformat(),
"period_end": current_time.isoformat(),
"digests_sent": 0,
"errors": []
}
# TODO: Get active agents and managers from database
mock_recipients = [
{
"user_id": "user-1",
"email": "agent1@example.com",
"name": "Agent One",
"role": "AGENT",
"tenant_id": "tenant-1"
},
{
"user_id": "user-2",
"email": "manager@example.com",
"name": "Support Manager",
"role": "SUPPORT_MANAGER",
"tenant_id": "tenant-1"
}
]
for recipient in mock_recipients:
try:
task_logger.info(
"Generating digest for user",
user_id=recipient["user_id"],
email=recipient["email"],
role=recipient["role"]
)
# TODO: Generate actual digest data from database
digest_data = generate_digest_data(
recipient["user_id"],
recipient["role"],
recipient["tenant_id"],
yesterday,
current_time
)
# Send digest email
send_templated_email_task.apply_async(kwargs={
"to_email": recipient["email"],
"template_name": "daily_digest",
"context": {
"user_name": recipient["name"],
"role": recipient["role"],
"date": current_time.strftime("%Y-%m-%d"),
**digest_data
},
"tenant_id": recipient["tenant_id"],
"correlation_id": self.request.id
})
digest_results["digests_sent"] += 1
except Exception as e:
error_msg = f"Failed to send digest to {recipient['email']}: {str(e)}"
digest_results["errors"].append(error_msg)
task_logger.error(
"Digest generation failed for user",
user_id=recipient["user_id"],
error=str(e)
)
task_logger.info(
"Daily digest generation completed",
digests_sent=digest_results["digests_sent"],
errors=len(digest_results["errors"])
)
return digest_results
except Exception as exc:
task_logger.error(
"Daily digest generation failed",
error=str(exc),
exc_info=True
)
raise
def generate_digest_data(
user_id: str,
role: str,
tenant_id: str,
period_start: datetime,
period_end: datetime
) -> Dict[str, Any]:
"""
Generate digest data for a specific user.
Args:
user_id: User ID
role: User role
tenant_id: Tenant ID
period_start: Start of digest period
period_end: End of digest period
Returns:
Dict with digest data
"""
# TODO: Implement actual database queries
# For now, return mock data
base_data = {
"summary": {
"new_tickets": 5,
"resolved_tickets": 7,
"pending_tickets": 12,
"overdue_tickets": 2
},
"sla_status": {
"response_sla_met": 8,
"response_sla_missed": 1,
"resolution_sla_met": 6,
"resolution_sla_missed": 2
}
}
if role == "AGENT":
base_data.update({
"assigned_tickets": [
{
"ticket_number": "TKT-2024-000001",
"subject": "Problema de conexión",
"priority": "HIGH",
"created_at": "2024-01-15T10:00:00Z",
"sla_due": "2024-01-15T12:00:00Z"
}
],
"urgent_tickets": 1,
"performance": {
"avg_response_time_hours": 1.5,
"avg_resolution_time_hours": 18.2,
"customer_satisfaction": 4.3
}
})
elif role in ["SUPPORT_MANAGER", "ADMIN"]:
base_data.update({
"team_summary": {
"total_agents": 5,
"active_agents": 4,
"avg_load_per_agent": 6.2
},
"escalations": [
{
"ticket_number": "TKT-2024-000002",
"reason": "SLA violation",
"assigned_to": "agent1@example.com"
}
],
"trends": {
"ticket_volume_change": "+12%",
"resolution_time_change": "-5%"
}
})
return base_data
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def send_ticket_notifications(
self,
ticket_id: str,
event_type: str,
tenant_id: str,
context: Dict[str, Any],
correlation_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Send ticket-related notifications.
Args:
ticket_id: Ticket ID
event_type: Type of event (created, assigned, updated, resolved, etc.)
tenant_id: Tenant ID
context: Context data for notifications
correlation_id: Correlation ID
Returns:
Dict with notification results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
ticket_id=ticket_id,
event_type=event_type,
tenant_id=tenant_id,
correlation_id=correlation_id
)
task_logger.info("Starting ticket notifications")
try:
notification_results = {
"ticket_id": ticket_id,
"event_type": event_type,
"notifications_sent": 0,
"notifications": []
}
# Determine who should receive notifications based on event type
recipients = get_notification_recipients(ticket_id, event_type, tenant_id)
for recipient in recipients:
try:
template_name = f"ticket_{event_type}"
# Send notification
result = send_templated_email_task.apply_async(kwargs={
"to_email": recipient["email"],
"template_name": template_name,
"context": {
**context,
"recipient_name": recipient["name"],
"recipient_role": recipient["role"]
},
"tenant_id": tenant_id,
"correlation_id": correlation_id or self.request.id
}).get()
notification_results["notifications"].append({
"recipient": recipient["email"],
"template": template_name,
"success": result.get("success", False),
"error": result.get("error")
})
if result.get("success"):
notification_results["notifications_sent"] += 1
except Exception as e:
task_logger.error(
"Failed to send notification",
recipient_email=recipient["email"],
error=str(e)
)
notification_results["notifications"].append({
"recipient": recipient["email"],
"success": False,
"error": str(e)
})
task_logger.info(
"Ticket notifications completed",
notifications_sent=notification_results["notifications_sent"],
total_recipients=len(recipients)
)
return notification_results
except Exception as exc:
task_logger.error(
"Ticket notifications failed",
error=str(exc),
exc_info=True
)
raise
def get_notification_recipients(
ticket_id: str,
event_type: str,
tenant_id: str
) -> List[Dict[str, Any]]:
"""
Get list of users who should receive notifications for a ticket event.
Args:
ticket_id: Ticket ID
event_type: Event type
tenant_id: Tenant ID
Returns:
List of recipient dicts
"""
# TODO: Implement actual database queries
# For now, return mock recipients based on event type
recipients = []
if event_type == "created":
# Notify assigned agent (if any) and customer
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
{"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
]
elif event_type == "assigned":
# Notify assigned agent and customer
recipients = [
{"email": "agent@example.com", "name": "Assigned Agent", "role": "AGENT"},
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
]
elif event_type == "updated":
# Notify all participants
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
{"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
]
elif event_type == "resolved":
# Notify customer for feedback
recipients = [
{"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
]
elif event_type == "escalated":
# Notify manager
recipients = [
{"email": "manager@example.com", "name": "Manager", "role": "SUPPORT_MANAGER"}
]
return recipients
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_system_alert(
self,
alert_type: str,
message: str,
severity: str = "INFO",
tenant_id: Optional[str] = None,
context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Send system alert to administrators.
Args:
alert_type: Type of alert (system_error, sla_violation, etc.)
message: Alert message
severity: Alert severity (INFO, WARNING, ERROR, CRITICAL)
tenant_id: Optional tenant ID
context: Additional context data
Returns:
Dict with alert results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
alert_type=alert_type,
severity=severity,
tenant_id=tenant_id
)
task_logger.info("Sending system alert", message=message)
try:
# TODO: Get administrators from configuration/database
admin_emails = ["admin@example.com", "alerts@example.com"]
alert_context = {
"alert_type": alert_type,
"message": message,
"severity": severity,
"timestamp": datetime.utcnow().isoformat(),
"environment": settings.ENVIRONMENT,
"tenant_id": tenant_id,
**(context or {})
}
notifications_sent = 0
for admin_email in admin_emails:
try:
send_templated_email_task.apply_async(kwargs={
"to_email": admin_email,
"template_name": "system_alert",
"context": alert_context,
"tenant_id": tenant_id,
"correlation_id": self.request.id
})
notifications_sent += 1
except Exception as e:
task_logger.error(
"Failed to send alert to admin",
admin_email=admin_email,
error=str(e)
)
task_logger.info(
"System alert sent",
notifications_sent=notifications_sent,
total_admins=len(admin_emails)
)
return {
"alert_type": alert_type,
"message": message,
"severity": severity,
"notifications_sent": notifications_sent,
"sent_at": datetime.utcnow().isoformat()
}
except Exception as exc:
task_logger.error(
"System alert failed",
error=str(exc),
exc_info=True
)
raise

View File

@@ -0,0 +1,327 @@
"""
SLA Tasks - ServiceManagerWeb Workers
Tareas para monitoreo y gestión de SLAs
"""
from celery import current_task
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import structlog
from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task
settings = get_settings()
logger = get_logger(__name__)
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def check_sla_violations(self) -> Dict[str, Any]:
"""
Check for SLA violations and send alerts.
This task runs every 5 minutes to check for:
- Response SLA violations
- Resolution SLA violations
- SLA warnings (approaching deadline)
Returns:
Dict with check results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name
)
task_logger.info("Starting SLA violations check")
if not settings.SLA_CHECK_ENABLED:
task_logger.info("SLA check disabled, skipping")
return {"status": "disabled"}
try:
current_time = datetime.utcnow()
results = {
"checked_at": current_time.isoformat(),
"response_violations": [],
"resolution_violations": [],
"warnings": [],
"notifications_sent": 0
}
# TODO: Implement actual database queries
# For now, simulate some checks
# Mock violations for development
mock_violations = [
{
"ticket_id": "mock-ticket-1",
"ticket_number": "TKT-2024-000001",
"subject": "Problema urgente de conexión",
"priority": "HIGH",
"sla_type": "response",
"due_at": (current_time - timedelta(minutes=30)).isoformat(),
"assigned_to_email": "agent@example.com",
"created_by_email": "cliente@example.com",
"tenant_id": "mock-tenant-1"
}
]
# Process violations
for violation in mock_violations:
task_logger.info(
"Processing SLA violation",
ticket_id=violation["ticket_id"],
sla_type=violation["sla_type"]
)
if violation["sla_type"] == "response":
results["response_violations"].append(violation)
# Send notification to assigned agent
if violation["assigned_to_email"]:
send_templated_email_task.apply_async(kwargs={
"to_email": violation["assigned_to_email"],
"template_name": "sla_response_violation",
"context": {
"ticket_number": violation["ticket_number"],
"subject": violation["subject"],
"priority": violation["priority"],
"due_at": violation["due_at"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
},
"tenant_id": violation["tenant_id"],
"correlation_id": self.request.id
})
results["notifications_sent"] += 1
elif violation["sla_type"] == "resolution":
results["resolution_violations"].append(violation)
# Send escalation notification
send_templated_email_task.apply_async(kwargs={
"to_email": "manager@example.com", # TODO: Get from tenant config
"template_name": "sla_resolution_violation",
"context": {
"ticket_number": violation["ticket_number"],
"subject": violation["subject"],
"priority": violation["priority"],
"assigned_to": violation["assigned_to_email"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
},
"tenant_id": violation["tenant_id"],
"correlation_id": self.request.id
})
results["notifications_sent"] += 1
# TODO: Check for SLA warnings (approaching deadline)
task_logger.info(
"SLA violations check completed",
response_violations=len(results["response_violations"]),
resolution_violations=len(results["resolution_violations"]),
warnings=len(results["warnings"]),
notifications_sent=results["notifications_sent"]
)
return results
except Exception as exc:
task_logger.error(
"SLA violations check failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=600,
soft_time_limit=540
)
def calculate_sla_metrics(self, tenant_id: str, date_from: str, date_to: str) -> Dict[str, Any]:
"""
Calculate SLA metrics for a tenant and date range.
Args:
tenant_id: Tenant ID
date_from: Start date (ISO format)
date_to: End date (ISO format)
Returns:
Dict with SLA metrics
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id
)
task_logger.info(
"Starting SLA metrics calculation",
date_from=date_from,
date_to=date_to
)
try:
# TODO: Implement actual database queries
# For now, return mock metrics
metrics = {
"tenant_id": tenant_id,
"date_from": date_from,
"date_to": date_to,
"calculated_at": datetime.utcnow().isoformat(),
"response_sla": {
"target_hours": 2,
"met_count": 45,
"total_count": 50,
"percentage": 90.0,
"avg_response_time_hours": 1.8
},
"resolution_sla": {
"target_hours": 24,
"met_count": 42,
"total_count": 48,
"percentage": 87.5,
"avg_resolution_time_hours": 22.5
},
"by_priority": {
"LOW": {
"response_sla_percentage": 95.0,
"resolution_sla_percentage": 90.0
},
"MEDIUM": {
"response_sla_percentage": 88.0,
"resolution_sla_percentage": 85.0
},
"HIGH": {
"response_sla_percentage": 92.0,
"resolution_sla_percentage": 88.0
},
"URGENT": {
"response_sla_percentage": 85.0,
"resolution_sla_percentage": 80.0
}
},
"trends": {
"response_sla_trend": "+2.5%",
"resolution_sla_trend": "-1.2%"
}
}
task_logger.info(
"SLA metrics calculation completed",
response_sla_percentage=metrics["response_sla"]["percentage"],
resolution_sla_percentage=metrics["resolution_sla"]["percentage"]
)
return metrics
except Exception as exc:
task_logger.error(
"SLA metrics calculation failed",
error=str(exc),
exc_info=True
)
raise
@celery_app.task(
bind=True,
time_limit=300,
soft_time_limit=240
)
def send_sla_warnings(self, tenant_id: Optional[str] = None) -> Dict[str, Any]:
"""
Send SLA warning notifications for tickets approaching deadline.
Args:
tenant_id: Optional tenant ID to filter by
Returns:
Dict with warning results
"""
task_logger = logger.bind(
task_id=self.request.id,
task_name=self.name,
tenant_id=tenant_id
)
task_logger.info("Starting SLA warnings check")
try:
current_time = datetime.utcnow()
warning_threshold = settings.SLA_WARNING_THRESHOLD # 80% of SLA time
# TODO: Query database for tickets approaching SLA deadlines
# Mock warnings
warnings = [
{
"ticket_id": "mock-ticket-2",
"ticket_number": "TKT-2024-000002",
"subject": "Consulta técnica",
"priority": "MEDIUM",
"sla_type": "response",
"due_at": (current_time + timedelta(minutes=30)).isoformat(),
"time_remaining_percent": 15.0,
"assigned_to_email": "agent@example.com",
"tenant_id": "mock-tenant-1"
}
]
notifications_sent = 0
for warning in warnings:
if warning["time_remaining_percent"] <= (1 - warning_threshold) * 100:
task_logger.info(
"Sending SLA warning",
ticket_id=warning["ticket_id"],
time_remaining_percent=warning["time_remaining_percent"]
)
send_templated_email_task.apply_async(kwargs={
"to_email": warning["assigned_to_email"],
"template_name": "sla_warning",
"context": {
"ticket_number": warning["ticket_number"],
"subject": warning["subject"],
"priority": warning["priority"],
"sla_type": warning["sla_type"],
"due_at": warning["due_at"],
"time_remaining_percent": warning["time_remaining_percent"],
"ticket_url": f"https://admin.servicemanager.local/tickets/{warning['ticket_id']}"
},
"tenant_id": warning["tenant_id"],
"correlation_id": self.request.id
})
notifications_sent += 1
task_logger.info(
"SLA warnings check completed",
warnings_found=len(warnings),
notifications_sent=notifications_sent
)
return {
"warnings_found": len(warnings),
"notifications_sent": notifications_sent,
"warnings": warnings
}
except Exception as exc:
task_logger.error(
"SLA warnings check failed",
error=str(exc),
exc_info=True
)
raise