vinindkoeb/app/routers/orders.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

113 lines
3.9 KiB
Python

import logging
from datetime import datetime, timezone
from decimal import Decimal
from typing import Optional
from fastapi import APIRouter, HTTPException, Query, status
from sqlalchemy.orm import selectinload
from sqlmodel import select
from app.db import SessionDep
from app.dependencies import CurrentUser, get_order_in_organization
from app.models.mail_template import MailEventType
from app.models.order import Order, OrderPublic, PaymentStatus
from app.models.order_line import OrderLine, OrderLinePublic
from app.models.purchase_round import PurchaseRound
from app.models.route import Route
from app.services.order_mail import send_order_event_mail
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/orders", tags=["orders"])
class OrderLineAdminView(OrderLinePublic):
wine_name: str
unit_price: Decimal
class OrderAdminView(OrderPublic):
participant_name: str
participant_email: str
participant_phone: Optional[str]
order_lines: list[OrderLineAdminView]
total_price_eur: Decimal
def _to_admin_view(order: Order) -> OrderAdminView:
order_lines = [
OrderLineAdminView(
id=ol.id,
order_id=ol.order_id,
wine_offering_id=ol.wine_offering_id,
quantity=ol.quantity,
wine_name=ol.wine_offering.name,
unit_price=ol.wine_offering.price,
)
for ol in order.order_lines
]
total_price_eur = sum((ol.unit_price * ol.quantity for ol in order_lines), Decimal("0"))
return OrderAdminView(
**OrderPublic.model_validate(order).model_dump(),
participant_name=order.participant.name,
participant_email=order.participant.email,
participant_phone=order.participant.phone,
order_lines=order_lines,
total_price_eur=total_price_eur,
)
@router.get("", response_model=list[OrderAdminView])
def list_orders(
session: SessionDep,
current_user: CurrentUser,
purchase_round_id: Optional[int] = None,
route_id: Optional[int] = None,
payment_status: Optional[PaymentStatus] = None,
limit: int = Query(default=100, le=500, gt=0),
offset: int = Query(default=0, ge=0),
) -> list[OrderAdminView]:
statement = (
select(Order)
.join(PurchaseRound)
.join(Route)
.where(Route.organization_id == current_user.organization_id)
.options(selectinload(Order.participant), selectinload(Order.order_lines).selectinload(OrderLine.wine_offering))
)
if purchase_round_id is not None:
statement = statement.where(Order.purchase_round_id == purchase_round_id)
if route_id is not None:
statement = statement.where(PurchaseRound.route_id == route_id)
if payment_status is not None:
statement = statement.where(Order.payment_status == payment_status)
statement = statement.order_by(Order.id.desc()).offset(offset).limit(limit)
orders = session.exec(statement).all()
return [_to_admin_view(order) for order in orders]
@router.post("/{order_id}/mark-paid", response_model=OrderAdminView)
def mark_order_paid(order_id: int, session: SessionDep, current_user: CurrentUser) -> OrderAdminView:
order = get_order_in_organization(session, order_id, current_user.organization_id)
if order.payment_status == PaymentStatus.PAID:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Order is already marked as paid")
order.payment_status = PaymentStatus.PAID
order.paid_at = datetime.now(timezone.utc)
session.add(order)
session.commit()
session.refresh(order)
try:
send_order_event_mail(
session,
order.purchase_round.route,
order.purchase_round,
order.participant,
order,
order.order_lines,
MailEventType.PAYMENT_CONFIRMED,
)
except Exception:
logger.exception("Failed to send payment_confirmed mail for order %s", order.id)
return _to_admin_view(order)