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>
108 lines
3.9 KiB
Python
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()
|