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