wahnsinn vibe

This commit is contained in:
Marek Lenczewski
2026-04-16 19:42:06 +02:00
parent 9c5da44f64
commit e3e88cc58e
127 changed files with 9456 additions and 3 deletions

View 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)