feat(phase-9): email dispatcher — HTML templates, delivery log, test endpoint
Backend: - app/models/email_log.py: EmailLog table (school_id, to, subject, type, status, error, sent_at) with EmailType + EmailStatus enums - migrations/002_phase9_email_logs.py: Alembic migration for email_logs table - app/templates/email/: 6 Jinja2 HTML templates — base layout, invoice, low_credit, license_expiry, overdue_warning, suspension - app/services/email.py: enhanced send_email() — accepts template_name+context for HTML rendering, logs every attempt to email_logs, retries up to 3x on transient SMTP failure with exponential backoff - app/routers/email.py: GET /api/email/logs (paginated, filterable by type/status/school), POST /api/email/test (send test email, super admin) - tasks/billing.py: invoice + overdue warning + suspension emails now use HTML templates - tasks/sms.py: low credit alert now uses HTML template - tasks/license.py: expiry warning now uses HTML template - app/main.py + migrations/env.py: wire in email_log model + email router Frontend: - EmailLogsPage.vue: table with to/subject/type badge/status badge/sent_at/error, type+status filters, pagination, Send Test Email modal - router/index.ts: /email-logs route - AppSidebar.vue: Email Logs nav item - api.ts: getEmailLogs, sendTestEmail
This commit is contained in:
94
backend/app/routers/email.py
Normal file
94
backend/app/routers/email.py
Normal file
@@ -0,0 +1,94 @@
|
||||
"""Email log viewer and test-email endpoint."""
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func, desc
|
||||
|
||||
from app.auth.dependencies import require_super_admin
|
||||
from app.database import get_db
|
||||
from app.models.user import HubUser
|
||||
from app.models.email_log import EmailLog, EmailType, EmailStatus
|
||||
|
||||
router = APIRouter(prefix="/api/email", tags=["email"])
|
||||
|
||||
|
||||
@router.get("/logs")
|
||||
async def list_email_logs(
|
||||
email_type: Optional[EmailType] = Query(None),
|
||||
status: Optional[EmailStatus] = Query(None),
|
||||
school_id: Optional[str] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
per_page: int = Query(50, ge=1, le=200),
|
||||
_admin: HubUser = Depends(require_super_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Paginated email delivery history — super admin only."""
|
||||
stmt = select(EmailLog).order_by(desc(EmailLog.created_at))
|
||||
|
||||
if email_type:
|
||||
stmt = stmt.where(EmailLog.email_type == email_type)
|
||||
if status:
|
||||
stmt = stmt.where(EmailLog.status == status)
|
||||
if school_id:
|
||||
stmt = stmt.where(EmailLog.school_id == school_id)
|
||||
|
||||
total = (await db.execute(select(func.count()).select_from(stmt.subquery()))).scalar_one()
|
||||
logs = (await db.execute(stmt.offset((page - 1) * per_page).limit(per_page))).scalars().all()
|
||||
|
||||
return {
|
||||
"items": [
|
||||
{
|
||||
"id": log.id,
|
||||
"school_id": log.school_id,
|
||||
"to_email": log.to_email,
|
||||
"subject": log.subject,
|
||||
"email_type": log.email_type.value,
|
||||
"status": log.status.value,
|
||||
"error_message": log.error_message,
|
||||
"sent_at": log.sent_at.isoformat() if log.sent_at else None,
|
||||
"created_at": log.created_at.isoformat(),
|
||||
}
|
||||
for log in logs
|
||||
],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"per_page": per_page,
|
||||
}
|
||||
|
||||
|
||||
class TestEmailBody(BaseModel):
|
||||
to: EmailStr
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
async def send_test_email(
|
||||
body: TestEmailBody,
|
||||
_admin: HubUser = Depends(require_super_admin),
|
||||
):
|
||||
"""Send a test email to verify SMTP configuration."""
|
||||
from app.services.email import send_email
|
||||
|
||||
success = send_email(
|
||||
to=body.to,
|
||||
subject="TapTrack Hub — SMTP Test Email",
|
||||
body=(
|
||||
"This is a test email from TapTrack Hub.\n\n"
|
||||
"If you received this, your SMTP configuration is working correctly.\n\n"
|
||||
"TapTrack Hub Team"
|
||||
),
|
||||
html=(
|
||||
"<div style='font-family:sans-serif;max-width:480px;margin:32px auto;padding:24px;"
|
||||
"background:#fff;border:1px solid #e2e8f0;border-radius:12px'>"
|
||||
"<h2 style='color:#1e40af;margin:0 0 16px'>TapTrack Hub — SMTP Test</h2>"
|
||||
"<p style='color:#334155'>This is a test email from <strong>TapTrack Hub</strong>.</p>"
|
||||
"<p style='color:#334155'>If you received this, your SMTP configuration is working correctly.</p>"
|
||||
"<p style='color:#94a3b8;font-size:12px;margin-top:24px'>TapTrack Hub Team</p>"
|
||||
"</div>"
|
||||
),
|
||||
email_type="test",
|
||||
)
|
||||
|
||||
if success:
|
||||
return {"message": f"Test email sent successfully to {body.to}"}
|
||||
return {"message": "Failed to send test email — check SMTP configuration and server logs"}
|
||||
Reference in New Issue
Block a user