Files
bacchus/apps/backend/app/api/topups.py
2025-09-28 19:13:01 +02:00

204 lines
6.4 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.

# app/api/topups.py
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from typing import List, Optional, Literal
from pydantic import BaseModel
from app.core.database import get_db
from app.core.auth import get_current_user, requires_role
from app.models.user import User
from app.models.topup import Topup, TopupStatus as DBTopupStatus
from app.schemas.topup import TopupOut, TopupCreate # Pydantic v2 Schemas
# Optionales Audit-Log nur nutzen, wenn vorhanden
try:
from app.models.audit_log import AuditLog, AuditAction
HAS_AUDIT = True
except Exception:
AuditLog = None
AuditAction = None
HAS_AUDIT = False
router = APIRouter(prefix="/topups", tags=["topups"])
# ---------------------------
# CREATE (User oder Admin)
# ---------------------------
@router.post("/", response_model=TopupOut, status_code=status.HTTP_201_CREATED)
def create_topup(
topup: TopupCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
Legt ein Topup an. amount_cents ist Pflicht. user_id ist optional; wenn nicht gesetzt,
wird der aktuelle User verwendet.
"""
payload = topup.model_dump() # Pydantic v2
# user_id für User-Flow ergänzen
user_id = payload.get("user_id") or current_user.id
amount_cents = int(payload.get("amount_cents") or 0)
if amount_cents <= 0:
raise HTTPException(status_code=400, detail="amount_cents must be > 0")
db_topup = Topup(
user_id=user_id,
amount_cents=amount_cents,
paypal_email=payload.get("paypal_email"),
note=payload.get("note"),
# status default: pending (siehe Model)
)
db.add(db_topup)
db.commit()
db.refresh(db_topup)
if HAS_AUDIT:
db.add(
AuditLog(
user_id=db_topup.user_id,
action=getattr(AuditAction, "topup_create", "topup_create"),
amount_cents=db_topup.amount_cents,
info=f"created by {current_user.id}",
)
)
db.commit()
return db_topup
# ---------------------------
# LIST (Admin/Manager, gefiltert)
# ---------------------------
@router.get("/", response_model=List[TopupOut], dependencies=[Depends(requires_role("manager", "admin"))])
def list_topups(
user_id: Optional[int] = Query(default=None),
status_filter: Optional[str] = Query(default=None, description="pending|confirmed|rejected"),
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = db.query(Topup)
if user_id is not None:
q = q.filter(Topup.user_id == user_id)
if status_filter:
try:
enum_val = DBTopupStatus(status_filter)
except ValueError:
raise HTTPException(status_code=400, detail="invalid status_filter")
q = q.filter(Topup.status == enum_val)
q = q.order_by(Topup.id.desc()).limit(limit).offset(offset)
return q.all()
# ---------------------------
# LIST OWN (alle eingeloggten)
# ---------------------------
@router.get("/me", response_model=List[TopupOut])
def list_my_topups(
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = (
db.query(Topup)
.filter(Topup.user_id == current_user.id)
.order_by(Topup.id.desc())
.limit(limit)
.offset(offset)
)
return q.all()
# ---------------------------
# STATUS ändern (Admin/Manager)
# ---------------------------
class TopupStatusIn(BaseModel):
status: Literal["confirmed", "rejected"]
@router.patch("/{topup_id}/status", dependencies=[Depends(requires_role("manager", "admin"))])
def patch_topup_status(
topup_id: int,
payload: TopupStatusIn,
db: Session = Depends(get_db),
admin: User = Depends(get_current_user),
):
t = db.query(Topup).filter(Topup.id == topup_id).first()
if not t:
raise HTTPException(status_code=404, detail="Topup not found")
# robustes Enum-Handling
old_enum = t.status if isinstance(t.status, DBTopupStatus) else DBTopupStatus(str(t.status))
try:
new_enum = DBTopupStatus(payload.status)
except ValueError:
raise HTTPException(status_code=400, detail="invalid status")
if old_enum == new_enum:
return {"status": "ok", "topup_id": t.id, "new_status": old_enum.value}
# Status setzen
t.status = new_enum
# Balance nur bei Übergang zu 'confirmed' buchen (idempotent)
if new_enum == DBTopupStatus.confirmed and old_enum != DBTopupStatus.confirmed:
u = db.query(User).filter(User.id == t.user_id).first()
if not u:
raise HTTPException(status_code=404, detail="User for topup not found")
before = int(u.balance_cents or 0)
u.balance_cents = before + int(t.amount_cents or 0)
if HAS_AUDIT:
db.add(
AuditLog(
user_id=u.id,
action=getattr(AuditAction, "topup_confirmed", "topup_confirmed"),
amount_cents=int(t.amount_cents or 0),
old_balance_cents=before,
new_balance_cents=u.balance_cents,
info=f"topup:{t.id} confirmed by {admin.id}",
)
)
# (Kein automatisches Reversal bei Wechsel zu rejected)
db.commit()
db.refresh(t)
return {"status": "ok", "topup_id": t.id, "new_status": t.status.value}
# ---------------------------
# DELETE (Admin/Manager)
# ---------------------------
@router.delete("/{topup_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(requires_role("manager", "admin"))])
def delete_topup(
topup_id: int,
db: Session = Depends(get_db),
admin: User = Depends(get_current_user),
):
t = db.query(Topup).filter(Topup.id == topup_id).first()
if not t:
raise HTTPException(status_code=404, detail="Topup not found")
db.delete(t)
db.commit()
if HAS_AUDIT:
db.add(
AuditLog(
user_id=t.user_id,
action=getattr(AuditAction, "topup_delete", "topup_delete"),
amount_cents=int(t.amount_cents or 0),
info=f"topup:{t.id} deleted by {admin.id}",
)
)
db.commit()
return None