from typing import Any from fastapi import APIRouter, Depends, HTTPException from jinja2 import Template from pydantic import BaseModel from sqlalchemy.orm import Session from core.db import get_db from core.di import get_service from core.events import event_bus from core.redis_client import redis_client from core.security import get_current_user_id, require_admin from core.settings_service import get_setting_cached from apps.auth.models import User from .models import Order, OrderStatusHistory router = APIRouter() _EMAIL_TEMPLATE_DE = """
Vielen Dank für deine Bestellung bei {{ shop_name }}!
Gesamt: {{ '%.2f' % order['total'] }} {{ order['currency'] }}
Lieferadresse: {{ order['address']['name'] }}, {{ order['address']['street'] }}, {{ order['address']['zip'] }} {{ order['address']['city'] }}
Zahlungsreferenz: {{ order['payment']['transaction_id'] }}
""" def _render_mail(order_dict: dict, shop_name: str) -> str: return Template(_EMAIL_TEMPLATE_DE).render(order=order_dict, shop_name=shop_name) # Event handler for checkout.confirmed ------------------------------------ def _on_checkout_confirmed(event_type: str, payload: dict[str, Any], db: Session) -> None: order = Order( user_id=payload.get("user_id"), status="paid", total=payload["total"], currency=payload.get("currency", "EUR"), address=payload.get("address", {}), payment=payload.get("payment", {}), items=payload.get("items", []), ) db.add(order) db.flush() db.add(OrderStatusHistory(order_id=order.id, status="paid", note="auto")) db.commit() # Convenience key for the synchronous checkout handler to return order_id if payload.get("user_id"): redis_client.set(f"user:{payload['user_id']}:last_order_id", str(order.id), ex=60) # Send confirmation mail user = db.get(User, order.user_id) if order.user_id else None if user: shop_name = get_setting_cached("core.shop_name", "Shop") body = _render_mail( { "id": order.id, "items": order.items, "total": float(order.total), "currency": order.currency, "address": order.address, "payment": order.payment, }, shop_name, ) try: mail = get_service("MailService") mail.send_sync(user.email, f"Bestellbestätigung #{order.id}", body) except Exception as e: # noqa: BLE001 print(f"[orders] mail send failed: {e}") event_bus.publish("order.created", {"id": order.id, "user_id": order.user_id}, db=db) def on_load() -> None: event_bus.subscribe("checkout.confirmed", _on_checkout_confirmed) # API ------------------------------------------------------------------- class OrderOut(BaseModel): id: int user_id: int | None status: str total: float currency: str address: dict payment: dict items: list created_at: str class StatusUpdateIn(BaseModel): status: str note: str = "" def _to_out(o: Order) -> OrderOut: return OrderOut( id=o.id, user_id=o.user_id, status=o.status, total=float(o.total), currency=o.currency, address=o.address or {}, payment=o.payment or {}, items=o.items or [], created_at=o.created_at.isoformat() if o.created_at else "", ) # Admin sub-router is registered FIRST so /admin* doesn't get shadowed by /{order_id} admin_router = APIRouter(dependencies=[Depends(require_admin)]) @admin_router.get("", response_model=list[OrderOut]) def admin_list_orders(db: Session = Depends(get_db)): rows = db.query(Order).order_by(Order.created_at.desc()).all() return [_to_out(o) for o in rows] @admin_router.get("/{order_id}", response_model=OrderOut) def admin_read_order(order_id: int, db: Session = Depends(get_db)): o = db.get(Order, order_id) if not o: raise HTTPException(404, "Not found") return _to_out(o) @admin_router.put("/{order_id}/status", response_model=OrderOut) def admin_update_status(order_id: int, body: StatusUpdateIn, db: Session = Depends(get_db)): o = db.get(Order, order_id) if not o: raise HTTPException(404, "Not found") o.status = body.status db.add(OrderStatusHistory(order_id=order_id, status=body.status, note=body.note)) db.commit() db.refresh(o) event_bus.publish("order.status_changed", {"id": order_id, "status": body.status}, db=db) return _to_out(o) router.include_router(admin_router, prefix="/admin") # Customer routes — defined AFTER admin include so '/admin' matches first @router.get("", response_model=list[OrderOut]) def list_my_orders(user_id: int = Depends(get_current_user_id), db: Session = Depends(get_db)): rows = ( db.query(Order) .filter(Order.user_id == user_id) .order_by(Order.created_at.desc()) .all() ) return [_to_out(o) for o in rows] @router.get("/{order_id}", response_model=OrderOut) def read_order(order_id: int, user_id: int = Depends(get_current_user_id), db: Session = Depends(get_db)): o = db.get(Order, order_id) if not o or o.user_id != user_id: raise HTTPException(404, "Order not found") return _to_out(o)