vinindkoeb/app/routers/mail_templates.py
carsten bf0affcdc6 Add mail template CRUD (task 7a), per-route and per-event-type
MailTemplate holds subject + body_html with plain {{variable}}
placeholders (no loops/conditionals — the user edits these directly
and is comfortable generating list-shaped content like the wine
catalog in code instead, per the real "Kvitering" email reviewed in
old-emails/). One template per (route, event_type), matching the
three CLAUDE.md mail-events (round_announced, order_confirmed,
payment_confirmed); UNIQUE(route_id, event_type) enforces that and
IntegrityError is caught as 409, same pattern as prior tasks.

Route gains nullable sender_name/sender_email — the real old emails
show a per-route sender identity (e.g. "Finn Gram <finn@vinindkoeb.dk>"
for this route), so it lives on Route rather than being duplicated
across each of its templates.

This is data + CRUD only. Actual Postal sending, the round-announce
action, and per-participant send logging are task 7b; a Postal
webhook receiver for delivery status is task 7c — both need real
Postal API access, still to be arranged.

old-emails/ (real historical order/participant data) is gitignored,
same treatment as old_participants.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 19:43:47 +02:00

73 lines
3 KiB
Python

from typing import Optional
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy.exc import IntegrityError
from sqlmodel import Session, select
from app.db import SessionDep
from app.dependencies import CurrentUser, get_mail_template_in_organization, get_route_in_organization
from app.models.mail_template import MailTemplate, MailTemplateCreate, MailTemplatePublic, MailTemplateUpdate
from app.models.route import Route
router = APIRouter(prefix="/mail-templates", tags=["mail-templates"])
def _commit_or_conflict(session: Session) -> None:
try:
session.commit()
except IntegrityError as exc:
session.rollback()
if "uq_mail_template_route_event" in str(getattr(exc, "orig", exc)):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="A mail template for this event type already exists on this route",
) from exc
raise
@router.post("", response_model=MailTemplatePublic, status_code=status.HTTP_201_CREATED)
def create_mail_template(payload: MailTemplateCreate, session: SessionDep, current_user: CurrentUser) -> MailTemplate:
get_route_in_organization(session, payload.route_id, current_user.organization_id)
mail_template = MailTemplate.model_validate(payload)
session.add(mail_template)
_commit_or_conflict(session)
session.refresh(mail_template)
return mail_template
@router.get("", response_model=list[MailTemplatePublic])
def list_mail_templates(
session: SessionDep,
current_user: CurrentUser,
route_id: Optional[int] = None,
) -> list[MailTemplate]:
statement = select(MailTemplate).join(Route).where(Route.organization_id == current_user.organization_id)
if route_id is not None:
statement = statement.where(MailTemplate.route_id == route_id)
statement = statement.order_by(MailTemplate.id)
return list(session.exec(statement).all())
@router.get("/{template_id}", response_model=MailTemplatePublic)
def get_mail_template(template_id: int, session: SessionDep, current_user: CurrentUser) -> MailTemplate:
return get_mail_template_in_organization(session, template_id, current_user.organization_id)
@router.patch("/{template_id}", response_model=MailTemplatePublic)
def update_mail_template(
template_id: int, payload: MailTemplateUpdate, session: SessionDep, current_user: CurrentUser
) -> MailTemplate:
mail_template = get_mail_template_in_organization(session, template_id, current_user.organization_id)
for field, value in payload.model_dump(exclude_unset=True).items():
setattr(mail_template, field, value)
session.add(mail_template)
_commit_or_conflict(session)
session.refresh(mail_template)
return mail_template
@router.delete("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_mail_template(template_id: int, session: SessionDep, current_user: CurrentUser) -> None:
mail_template = get_mail_template_in_organization(session, template_id, current_user.organization_id)
session.delete(mail_template)
session.commit()