vinindkoeb/app/services/order_mail.py
carsten b6b14aefac Opgave 10: fortløbende ordrenummer pr. runde + afhentningsliste
Order.order_number tildeles atomisk i POST /public/orders via en
SELECT ... FOR UPDATE-lås på rundens række + MAX+1, så samtidige
bestillinger lige efter en runde åbner ikke kan kollidere. Nummeret
starter på 1 for hver runde (UNIQUE(purchase_round_id, order_number))
og er nu tilgængeligt som {{order_number}} i mail-skabeloner.

GET /purchase-rounds/{id}/pickup-list genskaber den gamle systems
afhentningsliste (delt af brugeren som reference) mod den nye
datamodel: én printside pr. ordre, sorteret efter order_number (vist
stort i øverste højre hjørne), kontaktinfo, flaske-/kasseantal
(pænt afrundet — originalen viste et urundet float), og
vinlinjer grupperet pr. kategori.

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

108 lines
3.9 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_number": str(order.order_number),
"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()