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>
73 lines
3 KiB
Python
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()
|