This commit is contained in:
2025-09-28 19:13:01 +02:00
parent 49edf780b5
commit 541ecb48f2
67 changed files with 5176 additions and 5008 deletions

View File

@@ -0,0 +1,203 @@
# 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