Files
shop/backend/apps/orders/__init__.py
Marek Lenczewski e3e88cc58e wahnsinn vibe
2026-04-16 19:42:06 +02:00

176 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = """
<h1>Bestellbestätigung #{{ order.id }}</h1>
<p>Vielen Dank für deine Bestellung bei {{ shop_name }}!</p>
<h2>Artikel</h2>
<ul>
{% for it in order['items'] %}
<li>{{ it['name'].get('de', it['name'].get('en', it.get('sku',''))) }} — {{ it['qty'] }} × {{ '%.2f' % it['price'] }} {{ order['currency'] }}</li>
{% endfor %}
</ul>
<p><strong>Gesamt: {{ '%.2f' % order['total'] }} {{ order['currency'] }}</strong></p>
<p>Lieferadresse: {{ order['address']['name'] }}, {{ order['address']['street'] }}, {{ order['address']['zip'] }} {{ order['address']['city'] }}</p>
<p>Zahlungsreferenz: {{ order['payment']['transaction_id'] }}</p>
"""
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)