wahnsinn vibe
This commit is contained in:
175
backend/apps/orders/__init__.py
Normal file
175
backend/apps/orders/__init__.py
Normal file
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user