Files
Hotel-Booking/Backend/src/routes/booking_routes.py
2025-11-16 15:59:05 +02:00

438 lines
16 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from typing import Optional
from datetime import datetime
import random
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.booking import Booking, BookingStatus
from ..models.room import Room
from ..models.room_type import RoomType
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
router = APIRouter(prefix="/bookings", tags=["bookings"])
def generate_booking_number() -> str:
"""Generate unique booking number"""
prefix = "BK"
ts = int(datetime.utcnow().timestamp() * 1000)
rand = random.randint(1000, 9999)
return f"{prefix}-{ts}-{rand}"
@router.get("/")
async def get_all_bookings(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
startDate: Optional[str] = Query(None),
endDate: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get all bookings (Admin/Staff only)"""
try:
query = db.query(Booking)
# Filter by search (booking_number)
if search:
query = query.filter(Booking.booking_number.like(f"%{search}%"))
# Filter by status
if status_filter:
try:
query = query.filter(Booking.status == BookingStatus(status_filter))
except ValueError:
pass
# Filter by date range
if startDate:
start = datetime.fromisoformat(startDate.replace('Z', '+00:00'))
query = query.filter(Booking.check_in_date >= start)
if endDate:
end = datetime.fromisoformat(endDate.replace('Z', '+00:00'))
query = query.filter(Booking.check_in_date <= end)
# Get total count
total = query.count()
# Apply pagination
offset = (page - 1) * limit
bookings = query.order_by(Booking.created_at.desc()).offset(offset).limit(limit).all()
# Include related data
result = []
for booking in bookings:
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"user_id": booking.user_id,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add user info
if booking.user:
booking_dict["user"] = {
"id": booking.user.id,
"full_name": booking.user.full_name,
"email": booking.user.email,
"phone": booking.user.phone,
}
# Add room info
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
"floor": booking.room.floor,
}
result.append(booking_dict)
return {
"status": "success",
"data": {
"bookings": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/me")
async def get_my_bookings(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get current user's bookings"""
try:
bookings = db.query(Booking).filter(
Booking.user_id == current_user.id
).order_by(Booking.created_at.desc()).all()
result = []
for booking in bookings:
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add room info
if booking.room and booking.room.room_type:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
"room_type": {
"name": booking.room.room_type.name,
}
}
result.append(booking_dict)
return {
"success": True,
"data": {"bookings": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/")
async def create_booking(
booking_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new booking"""
try:
room_id = booking_data.get("room_id")
check_in_date = booking_data.get("check_in_date")
check_out_date = booking_data.get("check_out_date")
total_price = booking_data.get("total_price")
guest_count = booking_data.get("guest_count", 1)
notes = booking_data.get("notes")
payment_method = booking_data.get("payment_method", "cash")
if not all([room_id, check_in_date, check_out_date, total_price]):
raise HTTPException(status_code=400, detail="Missing required booking fields")
# Check if room exists
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
check_in = datetime.fromisoformat(check_in_date.replace('Z', '+00:00'))
check_out = datetime.fromisoformat(check_out_date.replace('Z', '+00:00'))
# Check for overlapping bookings
overlapping = db.query(Booking).filter(
and_(
Booking.room_id == room_id,
Booking.status != BookingStatus.cancelled,
Booking.check_in_date < check_out,
Booking.check_out_date > check_in
)
).first()
if overlapping:
raise HTTPException(
status_code=409,
detail="Room already booked for the selected dates"
)
booking_number = generate_booking_number()
# Determine if deposit is required
requires_deposit = payment_method == "cash"
deposit_percentage = 20 if requires_deposit else 0
deposit_amount = (float(total_price) * deposit_percentage) / 100 if requires_deposit else 0
# Create booking
booking = Booking(
booking_number=booking_number,
user_id=current_user.id,
room_id=room_id,
check_in_date=check_in,
check_out_date=check_out,
num_guests=guest_count,
total_price=total_price,
special_requests=notes,
status=BookingStatus.pending,
requires_deposit=requires_deposit,
deposit_paid=False,
)
db.add(booking)
db.flush()
# Create deposit payment if required
if requires_deposit:
payment = Payment(
booking_id=booking.id,
amount=deposit_amount,
payment_method=PaymentMethod.bank_transfer,
payment_type=PaymentType.deposit,
deposit_percentage=deposit_percentage,
payment_status=PaymentStatus.pending,
notes=f"Deposit payment ({deposit_percentage}%) for booking {booking_number}",
)
db.add(payment)
db.commit()
db.refresh(booking)
# Fetch with relations
booking = db.query(Booking).filter(Booking.id == booking.id).first()
return {
"success": True,
"data": {"booking": booking},
"message": f"Booking created. Please pay {deposit_percentage}% deposit to confirm." if requires_deposit else "Booking created successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_booking_by_id(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get booking by ID"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Check access
if current_user.role_id != 1 and booking.user_id != current_user.id: # Not admin
raise HTTPException(status_code=403, detail="Forbidden")
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"user_id": booking.user_id,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add relations
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
}
if booking.room.room_type:
booking_dict["room"]["room_type"] = {
"name": booking.room.room_type.name,
}
if booking.payments:
booking_dict["payments"] = [
{
"id": p.id,
"amount": float(p.amount) if p.amount else 0.0,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else p.payment_method,
"payment_status": p.payment_status.value if isinstance(p.payment_status, PaymentStatus) else p.payment_status,
}
for p in booking.payments
]
return {
"success": True,
"data": {"booking": booking_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/{id}/cancel")
async def cancel_booking(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Cancel a booking"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
if booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
if booking.status == BookingStatus.cancelled:
raise HTTPException(status_code=400, detail="Booking already cancelled")
booking.status = BookingStatus.cancelled
db.commit()
return {
"success": True,
"data": {"booking": booking}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_booking(
id: int,
booking_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update booking status (Admin only)"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
status_value = booking_data.get("status")
if status_value:
try:
booking.status = BookingStatus(status_value)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid status")
db.commit()
db.refresh(booking)
return {
"status": "success",
"message": "Booking updated successfully",
"data": {"booking": booking}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/check/{booking_number}")
async def check_booking_by_number(
booking_number: str,
db: Session = Depends(get_db)
):
"""Check booking by booking number"""
try:
booking = db.query(Booking).filter(Booking.booking_number == booking_number).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
}
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
}
return {
"status": "success",
"data": {"booking": booking_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))