vinindkoeb/app/services/order_mail.py
carsten 3592fbf845 Opgave 9: admin-ordreoversigt + "markér betalt"
GET /orders (filtre: purchase_round_id, route_id, payment_status)
giver admin en oversigt pr. ordre med deltagerinfo, vinlinjer og
beregnet total. POST /orders/{id}/mark-paid registrerer betalingen
(payment_status=paid, paid_at) og udløser mail-event #3
(payment_confirmed) — afviser med 409 hvis ordren allerede er betalt.

Udtrukket render/send/log-logikken fra 8a's _send_order_confirmation
til en delt app/services/order_mail.py::send_order_event_mail,
parameteriseret over MailEventType, så order_confirmed og
payment_confirmed ikke længere duplikerer den samme kode. Som med
order_confirmed er mail-afsendelsen best-effort: en manglende
skabelon eller afsenderkonfiguration blokerer aldrig selve
betalingsregistreringen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 00:03:45 +02:00

107 lines
3.8 KiB
Python

import logging
from datetime import datetime, timezone
from sqlalchemy.orm import selectinload
from sqlmodel import Session, select
from app.models.mail_log import MailLog, MailLogStatus
from app.models.mail_template import MailEventType, MailTemplate
from app.models.order import Order
from app.models.order_line import OrderLine
from app.models.participant import Participant
from app.models.purchase_round import PurchaseRound
from app.models.route import Route
from app.models.wine_offering import WineOffering
from app.services.mail_rendering import (
CatalogRow,
compute_order_total_eur,
format_currency,
format_datetime_da,
render_order_receipt_html,
render_template,
)
from app.services.postal import PostalSendError, send_mail
logger = logging.getLogger(__name__)
def send_order_event_mail(
session: Session,
route: Route,
purchase_round: PurchaseRound,
participant: Participant,
order: Order,
order_lines: list[OrderLine],
event_type: MailEventType,
) -> None:
template = session.exec(
select(MailTemplate).where(MailTemplate.route_id == route.id, MailTemplate.event_type == event_type)
).first()
if template is None:
logger.warning("No %s template for route %s — skipping mail", event_type.value, route.id)
return
if not route.sender_name or not route.sender_email:
logger.warning("Route %s has no sender_name/sender_email — skipping mail", route.id)
return
all_offerings = session.exec(
select(WineOffering)
.where(WineOffering.purchase_round_id == purchase_round.id)
.options(selectinload(WineOffering.category))
).all()
all_offerings_sorted = sorted(all_offerings, key=lambda o: (o.category.sort_order, o.id))
ordered_qty_by_offering = {ol.wine_offering_id: ol.quantity for ol in order_lines}
catalog_rows = [
CatalogRow(
category_name=o.category.name,
wine_name=o.name,
unit_price_eur=o.price,
ordered_quantity=ordered_qty_by_offering.get(o.id),
)
for o in all_offerings_sorted
]
total_eur = compute_order_total_eur(catalog_rows)
total_dkk = total_eur * purchase_round.eur_dkk_rate if purchase_round.eur_dkk_rate else None
variables = {
"participant_name": participant.name,
"route_name": route.name,
"round_name": purchase_round.name,
"order_deadline_at": format_datetime_da(purchase_round.order_deadline_at)
if purchase_round.order_deadline_at
else "",
"pickup_at": format_datetime_da(purchase_round.pickup_at) if purchase_round.pickup_at else "",
"pickup_info_text": purchase_round.pickup_info_text or "",
"order_lines_html": render_order_receipt_html(catalog_rows),
"order_total_eur": format_currency(total_eur),
"order_total_dkk": format_currency(total_dkk) if total_dkk is not None else "",
}
rendered_subject = render_template(template.subject, variables)
rendered_body = render_template(template.body_html, variables)
log = MailLog(
participant_id=participant.id,
purchase_round_id=purchase_round.id,
mail_template_id=template.id,
event_type=event_type,
rendered_subject=rendered_subject,
status=MailLogStatus.FAILED,
)
try:
postal_id, postal_token = send_mail(
to=participant.email,
from_name=route.sender_name,
from_email=route.sender_email,
subject=rendered_subject,
html_body=rendered_body,
tag=event_type.value,
)
log.status = MailLogStatus.SENT
log.postal_message_id = postal_id
log.postal_token = postal_token
log.sent_at = datetime.now(timezone.utc)
except PostalSendError as exc:
log.error_message = str(exc)
session.add(log)
session.commit()