438 lines
16 KiB
Python
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))
|