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>
107 lines
3.8 KiB
Python
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()
|