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

172 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 fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from app.core.database import get_db
from app.core.auth import get_current_user, requires_role
from app.models.booking import Booking
from app.models.user import User
from app.models.product import Product
from app.schemas.booking import BookingOut, BookingCreate
router = APIRouter(prefix="/bookings", tags=["bookings"])
# Konstante aus deinen Policies: Nutzer dürfen nicht < 0 € fallen (Stand 2025-07-02).
ALLOWED_OVERDRAFT_CENTS = 0 # falls -500 erlaubt sein soll: setze auf -500
@router.post("/", response_model=BookingOut, status_code=status.HTTP_201_CREATED)
def create_booking(
booking: BookingCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
if current_user.role not in ("manager", "admin") and booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
if booking.amount is None or booking.amount <= 0:
raise HTTPException(status_code=400, detail="Invalid amount")
# Produkt mit Row-Lock
product = (
db.query(Product)
.filter(Product.id == booking.product_id)
.with_for_update()
.first()
)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
if hasattr(product, "is_active") and not bool(product.is_active):
raise HTTPException(status_code=400, detail="Product is not active")
qty = int(booking.amount)
# Bestand prüfen/abziehen, falls Feld vorhanden
if hasattr(product, "stock"):
current_stock = int(product.stock or 0)
if current_stock < qty:
raise HTTPException(status_code=409, detail="Not enough stock")
product.stock = current_stock - qty
server_total = int(product.price_cents) * qty
try:
# User mit Row-Lock
user = (
db.query(User)
.filter(User.id == booking.user_id)
.with_for_update()
.first()
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
new_balance = int(user.balance_cents) - server_total
if new_balance < -ALLOWED_OVERDRAFT_CENTS:
raise HTTPException(status_code=400, detail="Insufficient balance")
user.balance_cents = new_balance
db_booking = Booking(
user_id=booking.user_id,
product_id=booking.product_id,
amount=qty,
total_cents=server_total, # Client-Wert wird ignoriert
)
db.add(db_booking)
db.flush()
db.refresh(db_booking)
db.commit()
return db_booking
except HTTPException:
db.rollback()
raise
except Exception:
db.rollback()
raise
def _role_name(r) -> str:
# robust gegen Enum/String/None
if r is None:
return ""
v = getattr(r, "value", r)
return str(v).strip().lower()
@router.get("/", response_model=List[BookingOut])
def list_bookings(
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
user_id: Optional[int] = Query(None, ge=1),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = db.query(Booking)
role = _role_name(getattr(current_user, "role", None))
is_manager = role in ("manager", "admin")
if is_manager:
# Admin/Manager: optionaler Filter auf user_id
if user_id is not None:
q = q.filter(Booking.user_id == user_id)
else:
# Normale Nutzer: strikt nur eigene Buchungen
q = q.filter(Booking.user_id == current_user.id)
# stabile, absteigende Reihenfolge Zeitfeld optional
order_cols = []
if hasattr(Booking, "created_at"):
order_cols.append(Booking.created_at.desc())
elif hasattr(Booking, "timestamp"):
order_cols.append(Booking.timestamp.desc())
elif hasattr(Booking, "created"):
order_cols.append(Booking.created.desc())
order_cols.append(Booking.id.desc())
return q.order_by(*order_cols).limit(limit).offset(offset).all()
@router.get("/me", response_model=List[BookingOut])
def list_my_bookings(
limit: int = Query(100, ge=1, le=1000),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = (
db.query(Booking)
.filter(Booking.user_id == current_user.id)
.order_by(Booking.id.desc())
.limit(limit)
.offset(offset)
)
return q.all()
@router.get("/{booking_id}", response_model=BookingOut)
def get_booking(
booking_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
b = db.query(Booking).filter(Booking.id == booking_id).first()
if not b:
raise HTTPException(status_code=404, detail="Booking not found")
if b.user_id != current_user.id and current_user.role not in ("manager", "admin"):
raise HTTPException(status_code=403, detail="Forbidden")
return b
@router.delete("/{booking_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_booking(
booking_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
requires_role("manager", "admin")(current_user)
b = db.query(Booking).filter(Booking.id == booking_id).first()
if not b:
raise HTTPException(status_code=404, detail="Booking not found")
db.delete(b)
db.commit()
return None