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