update to python fastpi

This commit is contained in:
Iliyan Angelov
2025-11-16 15:59:05 +02:00
parent 93d4c1df80
commit 98ccd5b6ff
4464 changed files with 773233 additions and 13740 deletions

View File

@@ -0,0 +1,2 @@
# Routes package

View File

@@ -0,0 +1,216 @@
from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from ..config.database import get_db
from ..services.auth_service import auth_service
from ..schemas.auth import (
RegisterRequest,
LoginRequest,
RefreshTokenRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
AuthResponse,
TokenResponse,
MessageResponse
)
from ..middleware.auth import get_current_user
from ..models.user import User
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequest,
response: Response,
db: Session = Depends(get_db)
):
"""Register new user"""
try:
result = await auth_service.register(
db=db,
name=request.name,
email=request.email,
password=request.password,
phone=request.phone
)
# Set refresh token as HttpOnly cookie
response.set_cookie(
key="refreshToken",
value=result["refreshToken"],
httponly=True,
secure=False, # Set to True in production with HTTPS
samesite="strict",
max_age=7 * 24 * 60 * 60, # 7 days
path="/"
)
# Format response to match frontend expectations
return {
"status": "success",
"message": "Registration successful",
"data": {
"token": result["token"],
"user": result["user"]
}
}
except ValueError as e:
error_message = str(e)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"status": "error",
"message": error_message
}
)
@router.post("/login")
async def login(
request: LoginRequest,
response: Response,
db: Session = Depends(get_db)
):
"""Login user"""
try:
result = await auth_service.login(
db=db,
email=request.email,
password=request.password,
remember_me=request.rememberMe or False
)
# Set refresh token as HttpOnly cookie
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
response.set_cookie(
key="refreshToken",
value=result["refreshToken"],
httponly=True,
secure=False, # Set to True in production with HTTPS
samesite="strict",
max_age=max_age,
path="/"
)
# Format response to match frontend expectations
return {
"status": "success",
"data": {
"token": result["token"],
"user": result["user"]
}
}
except ValueError as e:
error_message = str(e)
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message else status.HTTP_400_BAD_REQUEST
return JSONResponse(
status_code=status_code,
content={
"status": "error",
"message": error_message
}
)
@router.post("/refresh-token", response_model=TokenResponse)
async def refresh_token(
refreshToken: str = Cookie(None),
db: Session = Depends(get_db)
):
"""Refresh access token"""
if not refreshToken:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token not found"
)
try:
result = await auth_service.refresh_access_token(db, refreshToken)
return result
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
)
@router.post("/logout", response_model=MessageResponse)
async def logout(
response: Response,
refreshToken: str = Cookie(None),
db: Session = Depends(get_db)
):
"""Logout user"""
if refreshToken:
await auth_service.logout(db, refreshToken)
# Clear refresh token cookie
response.delete_cookie(key="refreshToken", path="/")
return {
"status": "success",
"message": "Logout successful"
}
@router.get("/profile")
async def get_profile(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get current user profile"""
try:
user = await auth_service.get_profile(db, current_user.id)
return user
except ValueError as e:
if "User not found" in str(e):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/forgot-password", response_model=MessageResponse)
async def forgot_password(
request: ForgotPasswordRequest,
db: Session = Depends(get_db)
):
"""Send password reset link"""
result = await auth_service.forgot_password(db, request.email)
return {
"status": "success",
"message": result["message"]
}
@router.post("/reset-password", response_model=MessageResponse)
async def reset_password(
request: ResetPasswordRequest,
db: Session = Depends(get_db)
):
"""Reset password with token"""
try:
result = await auth_service.reset_password(
db=db,
token=request.token,
password=request.password
)
return {
"status": "success",
"message": result["message"]
}
except ValueError as e:
status_code = status.HTTP_400_BAD_REQUEST
if "User not found" in str(e):
status_code = status.HTTP_404_NOT_FOUND
raise HTTPException(
status_code=status_code,
detail=str(e)
)

View File

@@ -0,0 +1,229 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from typing import Optional
from datetime import datetime
import os
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.banner import Banner
router = APIRouter(prefix="/banners", tags=["banners"])
def normalize_image_url(image_url: str, base_url: str) -> str:
"""Normalize image URL to absolute URL"""
if not image_url:
return image_url
if image_url.startswith('http://') or image_url.startswith('https://'):
return image_url
if image_url.startswith('/'):
return f"{base_url}{image_url}"
return f"{base_url}/{image_url}"
def get_base_url(request: Request) -> str:
"""Get base URL for image normalization"""
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
@router.get("/")
async def get_banners(
request: Request,
position: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
"""Get all active banners"""
try:
query = db.query(Banner).filter(Banner.is_active == True)
# Filter by position
if position:
query = query.filter(Banner.position == position)
# Filter by date range
now = datetime.utcnow()
query = query.filter(
or_(
Banner.start_date == None,
Banner.start_date <= now
)
).filter(
or_(
Banner.end_date == None,
Banner.end_date >= now
)
)
banners = query.order_by(Banner.display_order.asc(), Banner.created_at.desc()).all()
base_url = get_base_url(request)
result = []
for banner in banners:
banner_dict = {
"id": banner.id,
"title": banner.title,
"description": banner.description,
"image_url": normalize_image_url(banner.image_url, base_url),
"link_url": banner.link_url,
"position": banner.position,
"display_order": banner.display_order,
"is_active": banner.is_active,
"start_date": banner.start_date.isoformat() if banner.start_date else None,
"end_date": banner.end_date.isoformat() if banner.end_date else None,
"created_at": banner.created_at.isoformat() if banner.created_at else None,
}
result.append(banner_dict)
return {
"status": "success",
"data": {"banners": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_banner_by_id(
id: int,
request: Request,
db: Session = Depends(get_db)
):
"""Get banner by ID"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
base_url = get_base_url(request)
banner_dict = {
"id": banner.id,
"title": banner.title,
"description": banner.description,
"image_url": normalize_image_url(banner.image_url, base_url),
"link_url": banner.link_url,
"position": banner.position,
"display_order": banner.display_order,
"is_active": banner.is_active,
"start_date": banner.start_date.isoformat() if banner.start_date else None,
"end_date": banner.end_date.isoformat() if banner.end_date else None,
"created_at": banner.created_at.isoformat() if banner.created_at else None,
}
return {
"status": "success",
"data": {"banner": banner_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_banner(
banner_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new banner (Admin only)"""
try:
banner = Banner(
title=banner_data.get("title"),
description=banner_data.get("description"),
image_url=banner_data.get("image_url"),
link_url=banner_data.get("link"),
position=banner_data.get("position", "home"),
display_order=banner_data.get("display_order", 0),
is_active=True,
start_date=datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data.get("start_date") else None,
end_date=datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data.get("end_date") else None,
)
db.add(banner)
db.commit()
db.refresh(banner)
return {
"status": "success",
"message": "Banner created successfully",
"data": {"banner": banner}
}
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_banner(
id: int,
banner_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update banner (Admin only)"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
if "title" in banner_data:
banner.title = banner_data["title"]
if "description" in banner_data:
banner.description = banner_data["description"]
if "image_url" in banner_data:
banner.image_url = banner_data["image_url"]
if "link" in banner_data:
banner.link_url = banner_data["link"]
if "position" in banner_data:
banner.position = banner_data["position"]
if "display_order" in banner_data:
banner.display_order = banner_data["display_order"]
if "is_active" in banner_data:
banner.is_active = banner_data["is_active"]
if "start_date" in banner_data:
banner.start_date = datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data["start_date"] else None
if "end_date" in banner_data:
banner.end_date = datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data["end_date"] else None
db.commit()
db.refresh(banner)
return {
"status": "success",
"message": "Banner updated successfully",
"data": {"banner": banner}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_banner(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete banner (Admin only)"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
db.delete(banner)
db.commit()
return {
"status": "success",
"message": "Banner deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,437 @@
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))

View File

@@ -0,0 +1,187 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func
from ..config.database import get_db
from ..middleware.auth import get_current_user
from ..models.user import User
from ..models.favorite import Favorite
from ..models.room import Room
from ..models.room_type import RoomType
from ..models.review import Review, ReviewStatus
router = APIRouter(prefix="/favorites", tags=["favorites"])
@router.get("/")
async def get_favorites(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get user's favorite rooms"""
try:
favorites = db.query(Favorite).filter(
Favorite.user_id == current_user.id
).order_by(Favorite.created_at.desc()).all()
result = []
for favorite in favorites:
if not favorite.room:
continue
room = favorite.room
# Get review stats
review_stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
Review.room_id == room.id,
Review.status == ReviewStatus.approved
).first()
room_dict = {
"id": room.id,
"room_type_id": room.room_type_id,
"room_number": room.room_number,
"floor": room.floor,
"status": room.status.value if hasattr(room.status, 'value') else room.status,
"price": float(room.price) if room.price else 0.0,
"featured": room.featured,
"description": room.description,
"amenities": room.amenities,
"images": room.images or [],
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
}
if room.room_type:
room_dict["room_type"] = {
"id": room.room_type.id,
"name": room.room_type.name,
"description": room.room_type.description,
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
"capacity": room.room_type.capacity,
"amenities": room.room_type.amenities,
}
favorite_dict = {
"id": favorite.id,
"user_id": favorite.user_id,
"room_id": favorite.room_id,
"room": room_dict,
"created_at": favorite.created_at.isoformat() if favorite.created_at else None,
}
result.append(favorite_dict)
return {
"status": "success",
"data": {
"favorites": result,
"total": len(result),
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{room_id}")
async def add_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Add room to favorites"""
try:
# 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 if already favorited
existing = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
if existing:
raise HTTPException(
status_code=400,
detail="Room already in favorites list"
)
# Create favorite
favorite = Favorite(
user_id=current_user.id,
room_id=room_id
)
db.add(favorite)
db.commit()
db.refresh(favorite)
return {
"status": "success",
"message": "Added to favorites list",
"data": {"favorite": favorite}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{room_id}")
async def remove_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Remove room from favorites"""
try:
favorite = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
if not favorite:
raise HTTPException(
status_code=404,
detail="Room not found in favorites list"
)
db.delete(favorite)
db.commit()
return {
"status": "success",
"message": "Removed from favorites list"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/check/{room_id}")
async def check_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Check if room is favorited by user"""
try:
favorite = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
return {
"status": "success",
"data": {"isFavorited": favorite is not None}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,228 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
from ..models.booking import Booking
router = APIRouter(prefix="/payments", tags=["payments"])
@router.get("/")
async def get_payments(
booking_id: Optional[int] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get all payments"""
try:
query = db.query(Payment)
# Filter by booking_id
if booking_id:
query = query.filter(Payment.booking_id == booking_id)
# Filter by status
if status_filter:
try:
query = query.filter(Payment.payment_status == PaymentStatus(status_filter))
except ValueError:
pass
# Users can only see their own payments unless admin
if current_user.role_id != 1: # Not admin
query = query.join(Booking).filter(Booking.user_id == current_user.id)
total = query.count()
offset = (page - 1) * limit
payments = query.order_by(Payment.created_at.desc()).offset(offset).limit(limit).all()
result = []
for payment in payments:
payment_dict = {
"id": payment.id,
"booking_id": payment.booking_id,
"amount": float(payment.amount) if payment.amount else 0.0,
"payment_method": payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method,
"payment_type": payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type,
"deposit_percentage": payment.deposit_percentage,
"related_payment_id": payment.related_payment_id,
"payment_status": payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status,
"transaction_id": payment.transaction_id,
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
"notes": payment.notes,
"created_at": payment.created_at.isoformat() if payment.created_at else None,
}
if payment.booking:
payment_dict["booking"] = {
"id": payment.booking.id,
"booking_number": payment.booking.booking_number,
}
result.append(payment_dict)
return {
"status": "success",
"data": {
"payments": 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("/{id}")
async def get_payment_by_id(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get payment by ID"""
try:
payment = db.query(Payment).filter(Payment.id == id).first()
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
# Check access
if current_user.role_id != 1: # Not admin
if payment.booking and payment.booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
payment_dict = {
"id": payment.id,
"booking_id": payment.booking_id,
"amount": float(payment.amount) if payment.amount else 0.0,
"payment_method": payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method,
"payment_type": payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type,
"deposit_percentage": payment.deposit_percentage,
"related_payment_id": payment.related_payment_id,
"payment_status": payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status,
"transaction_id": payment.transaction_id,
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
"notes": payment.notes,
"created_at": payment.created_at.isoformat() if payment.created_at else None,
}
if payment.booking:
payment_dict["booking"] = {
"id": payment.booking.id,
"booking_number": payment.booking.booking_number,
}
return {
"status": "success",
"data": {"payment": payment_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/")
async def create_payment(
payment_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new payment"""
try:
booking_id = payment_data.get("booking_id")
amount = float(payment_data.get("amount", 0))
payment_method = payment_data.get("payment_method", "cash")
payment_type = payment_data.get("payment_type", "full")
# Check if booking exists
booking = db.query(Booking).filter(Booking.id == booking_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:
raise HTTPException(status_code=403, detail="Forbidden")
# Create payment
payment = Payment(
booking_id=booking_id,
amount=amount,
payment_method=PaymentMethod(payment_method),
payment_type=PaymentType(payment_type),
payment_status=PaymentStatus.pending,
payment_date=datetime.utcnow() if payment_data.get("mark_as_paid") else None,
notes=payment_data.get("notes"),
)
# If marked as paid, update status
if payment_data.get("mark_as_paid"):
payment.payment_status = PaymentStatus.completed
db.add(payment)
db.commit()
db.refresh(payment)
return {
"status": "success",
"message": "Payment created successfully",
"data": {"payment": payment}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/status", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def update_payment_status(
id: int,
status_data: dict,
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Update payment status (Admin/Staff only)"""
try:
payment = db.query(Payment).filter(Payment.id == id).first()
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
status_value = status_data.get("status")
if status_value:
try:
payment.payment_status = PaymentStatus(status_value)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payment status")
if status_data.get("transaction_id"):
payment.transaction_id = status_data["transaction_id"]
if status_data.get("mark_as_paid"):
payment.payment_status = PaymentStatus.completed
payment.payment_date = datetime.utcnow()
db.commit()
db.refresh(payment)
return {
"status": "success",
"message": "Payment status updated successfully",
"data": {"payment": payment}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,334 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.promotion import Promotion, DiscountType
router = APIRouter(prefix="/promotions", tags=["promotions"])
@router.get("/")
async def get_promotions(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Get all promotions with filters"""
try:
query = db.query(Promotion)
# Filter by search (code or name)
if search:
query = query.filter(
or_(
Promotion.code.like(f"%{search}%"),
Promotion.name.like(f"%{search}%")
)
)
# Filter by status (is_active)
if status_filter:
is_active = status_filter == "active"
query = query.filter(Promotion.is_active == is_active)
# Filter by discount type
if type:
try:
query = query.filter(Promotion.discount_type == DiscountType(type))
except ValueError:
pass
total = query.count()
offset = (page - 1) * limit
promotions = query.order_by(Promotion.created_at.desc()).offset(offset).limit(limit).all()
result = []
for promo in promotions:
promo_dict = {
"id": promo.id,
"code": promo.code,
"name": promo.name,
"description": promo.description,
"discount_type": promo.discount_type.value if isinstance(promo.discount_type, DiscountType) else promo.discount_type,
"discount_value": float(promo.discount_value) if promo.discount_value else 0.0,
"min_booking_amount": float(promo.min_booking_amount) if promo.min_booking_amount else None,
"max_discount_amount": float(promo.max_discount_amount) if promo.max_discount_amount else None,
"start_date": promo.start_date.isoformat() if promo.start_date else None,
"end_date": promo.end_date.isoformat() if promo.end_date else None,
"usage_limit": promo.usage_limit,
"used_count": promo.used_count,
"is_active": promo.is_active,
"created_at": promo.created_at.isoformat() if promo.created_at else None,
}
result.append(promo_dict)
return {
"status": "success",
"data": {
"promotions": 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("/{code}")
async def get_promotion_by_code(code: str, db: Session = Depends(get_db)):
"""Get promotion by code"""
try:
promotion = db.query(Promotion).filter(Promotion.code == code).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
promo_dict = {
"id": promotion.id,
"code": promotion.code,
"name": promotion.name,
"description": promotion.description,
"discount_type": promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type,
"discount_value": float(promotion.discount_value) if promotion.discount_value else 0.0,
"min_booking_amount": float(promotion.min_booking_amount) if promotion.min_booking_amount else None,
"max_discount_amount": float(promotion.max_discount_amount) if promotion.max_discount_amount else None,
"start_date": promotion.start_date.isoformat() if promotion.start_date else None,
"end_date": promotion.end_date.isoformat() if promotion.end_date else None,
"usage_limit": promotion.usage_limit,
"used_count": promotion.used_count,
"is_active": promotion.is_active,
}
return {
"status": "success",
"data": {"promotion": promo_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/validate")
async def validate_promotion(
validation_data: dict,
db: Session = Depends(get_db)
):
"""Validate and apply promotion"""
try:
code = validation_data.get("code")
booking_amount = float(validation_data.get("booking_amount", 0))
promotion = db.query(Promotion).filter(Promotion.code == code).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion code not found")
# Check if promotion is active
if not promotion.is_active:
raise HTTPException(status_code=400, detail="Promotion is not active")
# Check date validity
now = datetime.utcnow()
if promotion.start_date and now < promotion.start_date:
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
if promotion.end_date and now > promotion.end_date:
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
# Check usage limit
if promotion.usage_limit and promotion.used_count >= promotion.usage_limit:
raise HTTPException(status_code=400, detail="Promotion usage limit reached")
# Check minimum booking amount
if promotion.min_booking_amount and booking_amount < float(promotion.min_booking_amount):
raise HTTPException(
status_code=400,
detail=f"Minimum booking amount is {promotion.min_booking_amount}"
)
# Calculate discount
discount_amount = promotion.calculate_discount(booking_amount)
final_amount = booking_amount - discount_amount
return {
"status": "success",
"data": {
"promotion": {
"id": promotion.id,
"code": promotion.code,
"name": promotion.name,
},
"original_amount": booking_amount,
"discount_amount": discount_amount,
"final_amount": final_amount,
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_promotion(
promotion_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new promotion (Admin only)"""
try:
code = promotion_data.get("code")
# Check if code exists
existing = db.query(Promotion).filter(Promotion.code == code).first()
if existing:
raise HTTPException(status_code=400, detail="Promotion code already exists")
discount_type = promotion_data.get("discount_type")
discount_value = float(promotion_data.get("discount_value", 0))
# Validate discount value
if discount_type == "percentage" and discount_value > 100:
raise HTTPException(
status_code=400,
detail="Percentage discount cannot exceed 100%"
)
promotion = Promotion(
code=code,
name=promotion_data.get("name"),
description=promotion_data.get("description"),
discount_type=DiscountType(discount_type),
discount_value=discount_value,
min_booking_amount=float(promotion_data["min_booking_amount"]) if promotion_data.get("min_booking_amount") else None,
max_discount_amount=float(promotion_data["max_discount_amount"]) if promotion_data.get("max_discount_amount") else None,
start_date=datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data.get("start_date") else None,
end_date=datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data.get("end_date") else None,
usage_limit=promotion_data.get("usage_limit"),
used_count=0,
is_active=promotion_data.get("status") == "active" if promotion_data.get("status") else True,
)
db.add(promotion)
db.commit()
db.refresh(promotion)
return {
"status": "success",
"message": "Promotion created successfully",
"data": {"promotion": promotion}
}
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_promotion(
id: int,
promotion_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update promotion (Admin only)"""
try:
promotion = db.query(Promotion).filter(Promotion.id == id).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
# Check if new code exists (excluding current)
code = promotion_data.get("code")
if code and code != promotion.code:
existing = db.query(Promotion).filter(
Promotion.code == code,
Promotion.id != id
).first()
if existing:
raise HTTPException(status_code=400, detail="Promotion code already exists")
# Validate discount value
discount_type = promotion_data.get("discount_type", promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type)
discount_value = promotion_data.get("discount_value")
if discount_value is not None:
discount_value = float(discount_value)
if discount_type == "percentage" and discount_value > 100:
raise HTTPException(
status_code=400,
detail="Percentage discount cannot exceed 100%"
)
# Update fields
if "code" in promotion_data:
promotion.code = promotion_data["code"]
if "name" in promotion_data:
promotion.name = promotion_data["name"]
if "description" in promotion_data:
promotion.description = promotion_data["description"]
if "discount_type" in promotion_data:
promotion.discount_type = DiscountType(promotion_data["discount_type"])
if "discount_value" in promotion_data:
promotion.discount_value = discount_value
if "min_booking_amount" in promotion_data:
promotion.min_booking_amount = float(promotion_data["min_booking_amount"]) if promotion_data["min_booking_amount"] else None
if "max_discount_amount" in promotion_data:
promotion.max_discount_amount = float(promotion_data["max_discount_amount"]) if promotion_data["max_discount_amount"] else None
if "start_date" in promotion_data:
promotion.start_date = datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data["start_date"] else None
if "end_date" in promotion_data:
promotion.end_date = datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data["end_date"] else None
if "usage_limit" in promotion_data:
promotion.usage_limit = promotion_data["usage_limit"]
if "status" in promotion_data:
promotion.is_active = promotion_data["status"] == "active"
db.commit()
db.refresh(promotion)
return {
"status": "success",
"message": "Promotion updated successfully",
"data": {"promotion": promotion}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_promotion(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete promotion (Admin only)"""
try:
promotion = db.query(Promotion).filter(Promotion.id == id).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
db.delete(promotion)
db.commit()
return {
"status": "success",
"message": "Promotion deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,288 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from typing import Optional
from datetime import datetime, timedelta
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.payment import Payment, PaymentStatus
from ..models.room import Room
router = APIRouter(prefix="/reports", tags=["reports"])
@router.get("")
async def get_reports(
from_date: Optional[str] = Query(None, alias="from"),
to_date: Optional[str] = Query(None, alias="to"),
type: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get comprehensive reports (Admin/Staff only)"""
try:
# Parse dates if provided
start_date = None
end_date = None
if from_date:
try:
start_date = datetime.strptime(from_date, "%Y-%m-%d")
except ValueError:
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
if to_date:
try:
end_date = datetime.strptime(to_date, "%Y-%m-%d")
# Set to end of day
end_date = end_date.replace(hour=23, minute=59, second=59)
except ValueError:
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
# Base queries
booking_query = db.query(Booking)
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
# Apply date filters
if start_date:
booking_query = booking_query.filter(Booking.created_at >= start_date)
payment_query = payment_query.filter(Payment.payment_date >= start_date)
if end_date:
booking_query = booking_query.filter(Booking.created_at <= end_date)
payment_query = payment_query.filter(Payment.payment_date <= end_date)
# Total bookings
total_bookings = booking_query.count()
# Total revenue
total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0
# Total customers (unique users with bookings)
total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0
if start_date or end_date:
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
if start_date:
customer_query = customer_query.filter(Booking.created_at >= start_date)
if end_date:
customer_query = customer_query.filter(Booking.created_at <= end_date)
total_customers = customer_query.scalar() or 0
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Occupied rooms (rooms with active bookings)
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])
).scalar() or 0
# Revenue by date (daily breakdown)
revenue_by_date = []
if start_date and end_date:
daily_revenue_query = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('revenue'),
func.count(func.distinct(Payment.booking_id)).label('bookings')
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
if end_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date <= end_date)
daily_revenue_query = daily_revenue_query.group_by(
func.date(Payment.payment_date)
).order_by(func.date(Payment.payment_date))
daily_data = daily_revenue_query.all()
revenue_by_date = [
{
"date": str(date),
"revenue": float(revenue or 0),
"bookings": int(bookings or 0)
}
for date, revenue, bookings in daily_data
]
# Bookings by status
bookings_by_status = {}
for status in BookingStatus:
count = booking_query.filter(Booking.status == status).count()
status_name = status.value if hasattr(status, 'value') else str(status)
bookings_by_status[status_name] = count
# Top rooms (by revenue)
top_rooms_query = db.query(
Room.id,
Room.room_number,
func.count(Booking.id).label('bookings'),
func.sum(Payment.amount).label('revenue')
).join(Booking, Room.id == Booking.room_id).join(
Payment, Booking.id == Payment.booking_id
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
if end_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at <= end_date)
top_rooms_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(
func.sum(Payment.amount).desc()
).limit(10).all()
top_rooms = [
{
"room_id": room_id,
"room_number": room_number,
"bookings": int(bookings or 0),
"revenue": float(revenue or 0)
}
for room_id, room_number, bookings, revenue in top_rooms_data
]
return {
"status": "success",
"success": True,
"data": {
"total_bookings": total_bookings,
"total_revenue": float(total_revenue),
"total_customers": int(total_customers),
"available_rooms": available_rooms,
"occupied_rooms": occupied_rooms,
"revenue_by_date": revenue_by_date if revenue_by_date else None,
"bookings_by_status": bookings_by_status,
"top_rooms": top_rooms if top_rooms else None,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/dashboard")
async def get_dashboard_stats(
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get dashboard statistics (Admin/Staff only)"""
try:
# Total bookings
total_bookings = db.query(Booking).count()
# Active bookings
active_bookings = db.query(Booking).filter(
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
# Total revenue (from completed payments)
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Today's revenue
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
today_revenue = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.payment_status == PaymentStatus.completed,
Payment.payment_date >= today_start
)
).scalar() or 0.0
# Total rooms
total_rooms = db.query(Room).count()
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Recent bookings (last 7 days)
week_ago = datetime.utcnow() - timedelta(days=7)
recent_bookings = db.query(Booking).filter(
Booking.created_at >= week_ago
).count()
# Pending payments
pending_payments = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.pending
).count()
return {
"status": "success",
"data": {
"total_bookings": total_bookings,
"active_bookings": active_bookings,
"total_revenue": float(total_revenue),
"today_revenue": float(today_revenue),
"total_rooms": total_rooms,
"available_rooms": available_rooms,
"recent_bookings": recent_bookings,
"pending_payments": pending_payments,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/revenue")
async def get_revenue_report(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get revenue report (Admin/Staff only)"""
try:
query = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.completed
)
if start_date:
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date >= start)
if end_date:
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date <= end)
# Total revenue
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Revenue by payment method
revenue_by_method = db.query(
Payment.payment_method,
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(Payment.payment_method).all()
method_breakdown = {}
for method, total in revenue_by_method:
method_name = method.value if hasattr(method, 'value') else str(method)
method_breakdown[method_name] = float(total or 0)
# Revenue by date (daily breakdown)
daily_revenue = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all()
daily_breakdown = [
{
"date": date.isoformat() if isinstance(date, datetime) else str(date),
"revenue": float(total or 0)
}
for date, total in daily_revenue
]
return {
"status": "success",
"data": {
"total_revenue": float(total_revenue),
"revenue_by_method": method_breakdown,
"daily_breakdown": daily_breakdown,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,251 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.review import Review, ReviewStatus
from ..models.room import Room
router = APIRouter(prefix="/reviews", tags=["reviews"])
@router.get("/room/{room_id}")
async def get_room_reviews(room_id: int, db: Session = Depends(get_db)):
"""Get reviews for a room"""
try:
reviews = db.query(Review).filter(
Review.room_id == room_id,
Review.status == ReviewStatus.approved
).order_by(Review.created_at.desc()).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
}
result.append(review_dict)
return {
"status": "success",
"data": {"reviews": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/", dependencies=[Depends(authorize_roles("admin"))])
async def get_all_reviews(
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get all reviews (Admin only)"""
try:
query = db.query(Review)
if status_filter:
try:
query = query.filter(Review.status == ReviewStatus(status_filter))
except ValueError:
pass
total = query.count()
offset = (page - 1) * limit
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
"phone": review.user.phone,
}
if review.room:
review_dict["room"] = {
"id": review.room.id,
"room_number": review.room.room_number,
}
result.append(review_dict)
return {
"status": "success",
"data": {
"reviews": 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.post("/")
async def create_review(
review_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new review"""
try:
room_id = review_data.get("room_id")
rating = review_data.get("rating")
comment = review_data.get("comment")
# 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 if user already reviewed this room
existing = db.query(Review).filter(
Review.user_id == current_user.id,
Review.room_id == room_id
).first()
if existing:
raise HTTPException(
status_code=400,
detail="You have already reviewed this room"
)
# Create review
review = Review(
user_id=current_user.id,
room_id=room_id,
rating=rating,
comment=comment,
status=ReviewStatus.pending,
)
db.add(review)
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review submitted successfully and is pending approval",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/approve", dependencies=[Depends(authorize_roles("admin"))])
async def approve_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Approve review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
review.status = ReviewStatus.approved
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review approved successfully",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/reject", dependencies=[Depends(authorize_roles("admin"))])
async def reject_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Reject review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
review.status = ReviewStatus.rejected
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review rejected successfully",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
db.delete(review)
db.commit()
return {
"status": "success",
"message": "Review deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,517 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from typing import List, Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.room import Room, RoomStatus
from ..models.room_type import RoomType
from ..models.review import Review, ReviewStatus
from ..models.booking import Booking, BookingStatus
from ..services.room_service import get_rooms_with_ratings, get_amenities_list, normalize_images, get_base_url
import os
import aiofiles
from pathlib import Path
router = APIRouter(prefix="/rooms", tags=["rooms"])
@router.get("/")
async def get_rooms(
request: Request,
type: Optional[str] = Query(None),
minPrice: Optional[float] = Query(None),
maxPrice: Optional[float] = Query(None),
capacity: Optional[int] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
sort: Optional[str] = Query(None),
featured: Optional[bool] = Query(None),
db: Session = Depends(get_db)
):
"""Get all rooms with filters"""
try:
# Build where clause for rooms
where_clause = {}
room_type_where = {}
if featured is not None:
where_clause["featured"] = featured
if type:
room_type_where["name"] = f"%{type}%"
if capacity:
room_type_where["capacity"] = capacity
if minPrice or maxPrice:
if minPrice:
room_type_where["base_price_min"] = minPrice
if maxPrice:
room_type_where["base_price_max"] = maxPrice
# Build query
query = db.query(Room).join(RoomType)
# Apply filters
if where_clause.get("featured") is not None:
query = query.filter(Room.featured == where_clause["featured"])
if room_type_where.get("name"):
query = query.filter(RoomType.name.like(room_type_where["name"]))
if room_type_where.get("capacity"):
query = query.filter(RoomType.capacity >= room_type_where["capacity"])
if room_type_where.get("base_price_min"):
query = query.filter(RoomType.base_price >= room_type_where["base_price_min"])
if room_type_where.get("base_price_max"):
query = query.filter(RoomType.base_price <= room_type_where["base_price_max"])
# Get total count
total = query.count()
# Apply sorting
if sort == "newest" or sort == "created_at":
query = query.order_by(Room.created_at.desc())
else:
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
# Apply pagination
offset = (page - 1) * limit
rooms = query.offset(offset).limit(limit).all()
# Get base URL
base_url = get_base_url(request)
# Get rooms with ratings
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
return {
"status": "success",
"data": {
"rooms": rooms_with_ratings,
"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("/amenities")
async def get_amenities(db: Session = Depends(get_db)):
"""Get all available amenities"""
try:
amenities = await get_amenities_list(db)
return {"status": "success", "data": {"amenities": amenities}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/available")
async def search_available_rooms(
request: Request,
from_date: str = Query(..., alias="from"),
to_date: str = Query(..., alias="to"),
type: Optional[str] = Query(None),
capacity: Optional[int] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(12, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Search for available rooms"""
try:
check_in = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
check_out = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
if check_in >= check_out:
raise HTTPException(
status_code=400,
detail="Check-out date must be after check-in date"
)
# Build room type filter
query = db.query(Room).join(RoomType).filter(Room.status == RoomStatus.available)
if type:
query = query.filter(RoomType.name.like(f"%{type}%"))
if capacity:
query = query.filter(RoomType.capacity >= capacity)
# Exclude rooms with overlapping bookings
overlapping_rooms = db.query(Booking.room_id).filter(
and_(
Booking.status != BookingStatus.cancelled,
Booking.check_in_date < check_out,
Booking.check_out_date > check_in
)
).subquery()
query = query.filter(~Room.id.in_(db.query(overlapping_rooms.c.room_id)))
# Get total
total = query.count()
# Apply sorting and pagination
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
offset = (page - 1) * limit
rooms = query.offset(offset).limit(limit).all()
# Get base URL
base_url = get_base_url(request)
# Get rooms with ratings
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
return {
"status": "success",
"data": {
"rooms": rooms_with_ratings,
"search": {
"from": from_date,
"to": to_date,
"type": type,
"capacity": capacity,
},
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_room_by_id(id: int, request: Request, db: Session = Depends(get_db)):
"""Get room by ID"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Get review stats
review_stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
and_(
Review.room_id == room.id,
Review.status == ReviewStatus.approved
)
).first()
base_url = get_base_url(request)
room_dict = {
"id": room.id,
"room_type_id": room.room_type_id,
"room_number": room.room_number,
"floor": room.floor,
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
"price": float(room.price) if room.price else 0.0,
"featured": room.featured,
"description": room.description,
"amenities": room.amenities,
"created_at": room.created_at.isoformat() if room.created_at else None,
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
}
# Normalize images
try:
room_dict["images"] = normalize_images(room.images, base_url)
except:
room_dict["images"] = []
# Add room type
if room.room_type:
room_dict["room_type"] = {
"id": room.room_type.id,
"name": room.room_type.name,
"description": room.room_type.description,
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
"capacity": room.room_type.capacity,
"amenities": room.room_type.amenities,
"images": [] # RoomType doesn't have images column in DB
}
return {
"status": "success",
"data": {"room": room_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_room(
room_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new room (Admin only)"""
try:
# Check if room type exists
room_type = db.query(RoomType).filter(RoomType.id == room_data.get("room_type_id")).first()
if not room_type:
raise HTTPException(status_code=404, detail="Room type not found")
# Check if room number exists
existing = db.query(Room).filter(Room.room_number == room_data.get("room_number")).first()
if existing:
raise HTTPException(status_code=400, detail="Room number already exists")
room = Room(
room_type_id=room_data.get("room_type_id"),
room_number=room_data.get("room_number"),
floor=room_data.get("floor"),
status=RoomStatus(room_data.get("status", "available")),
featured=room_data.get("featured", False),
price=room_data.get("price", room_type.base_price),
)
db.add(room)
db.commit()
db.refresh(room)
return {
"status": "success",
"message": "Room created successfully",
"data": {"room": room}
}
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_room(
id: int,
room_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update room (Admin only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
if room_data.get("room_type_id"):
room_type = db.query(RoomType).filter(RoomType.id == room_data["room_type_id"]).first()
if not room_type:
raise HTTPException(status_code=404, detail="Room type not found")
# Update fields
if "room_type_id" in room_data:
room.room_type_id = room_data["room_type_id"]
if "room_number" in room_data:
room.room_number = room_data["room_number"]
if "floor" in room_data:
room.floor = room_data["floor"]
if "status" in room_data:
room.status = RoomStatus(room_data["status"])
if "featured" in room_data:
room.featured = room_data["featured"]
if "price" in room_data:
room.price = room_data["price"]
db.commit()
db.refresh(room)
return {
"status": "success",
"message": "Room updated successfully",
"data": {"room": room}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_room(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete room (Admin only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
db.delete(room)
db.commit()
return {
"status": "success",
"message": "Room deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def upload_room_images(
id: int,
images: List[UploadFile] = File(...),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Upload room images (Admin/Staff only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Create uploads directory
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "rooms"
upload_dir.mkdir(parents=True, exist_ok=True)
image_urls = []
for image in images:
# Validate file type
if not image.content_type.startswith('image/'):
continue
# Generate filename
import uuid
ext = Path(image.filename).suffix
filename = f"room-{uuid.uuid4()}{ext}"
file_path = upload_dir / filename
# Save file
async with aiofiles.open(file_path, 'wb') as f:
content = await image.read()
await f.write(content)
image_urls.append(f"/uploads/rooms/{filename}")
# Update room images (images are stored on Room, not RoomType)
existing_images = room.images or []
updated_images = existing_images + image_urls
room.images = updated_images
db.commit()
return {
"status": "success",
"message": "Images uploaded successfully",
"data": {"images": updated_images}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def delete_room_images(
id: int,
image_url: str,
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Delete room images (Admin/Staff only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Update room images (images are stored on Room, not RoomType)
existing_images = room.images or []
updated_images = [img for img in existing_images if img != image_url]
# Delete file from disk
filename = Path(image_url).name
file_path = Path(__file__).parent.parent.parent / "uploads" / "rooms" / filename
if file_path.exists():
file_path.unlink()
room.images = updated_images
db.commit()
return {
"status": "success",
"message": "Image deleted successfully",
"data": {"images": updated_images}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}/reviews")
async def get_room_reviews_route(
id: int,
db: Session = Depends(get_db)
):
"""Get reviews for a specific room"""
from ..models.review import Review, ReviewStatus
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
reviews = db.query(Review).filter(
Review.room_id == id,
Review.status == ReviewStatus.approved
).order_by(Review.created_at.desc()).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
}
result.append(review_dict)
return {
"status": "success",
"data": {"reviews": result}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,277 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.service import Service
from ..models.service_usage import ServiceUsage
from ..models.booking import Booking, BookingStatus
router = APIRouter(prefix="/services", tags=["services"])
@router.get("/")
async def get_services(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Get all services with filters"""
try:
query = db.query(Service)
# Filter by search (name or description)
if search:
query = query.filter(
or_(
Service.name.like(f"%{search}%"),
Service.description.like(f"%{search}%")
)
)
# Filter by status (is_active)
if status_filter:
is_active = status_filter == "active"
query = query.filter(Service.is_active == is_active)
total = query.count()
offset = (page - 1) * limit
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
result = []
for service in services:
service_dict = {
"id": service.id,
"name": service.name,
"description": service.description,
"price": float(service.price) if service.price else 0.0,
"category": service.category,
"is_active": service.is_active,
"created_at": service.created_at.isoformat() if service.created_at else None,
}
result.append(service_dict)
return {
"status": "success",
"data": {
"services": 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("/{id}")
async def get_service_by_id(id: int, db: Session = Depends(get_db)):
"""Get service by ID"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
service_dict = {
"id": service.id,
"name": service.name,
"description": service.description,
"price": float(service.price) if service.price else 0.0,
"category": service.category,
"is_active": service.is_active,
"created_at": service.created_at.isoformat() if service.created_at else None,
}
return {
"status": "success",
"data": {"service": service_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_service(
service_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new service (Admin only)"""
try:
name = service_data.get("name")
# Check if name exists
existing = db.query(Service).filter(Service.name == name).first()
if existing:
raise HTTPException(status_code=400, detail="Service name already exists")
service = Service(
name=name,
description=service_data.get("description"),
price=float(service_data.get("price", 0)),
category=service_data.get("category"),
is_active=service_data.get("status") == "active" if service_data.get("status") else True,
)
db.add(service)
db.commit()
db.refresh(service)
return {
"status": "success",
"message": "Service created successfully",
"data": {"service": service}
}
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_service(
id: int,
service_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update service (Admin only)"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
# Check if new name exists (excluding current)
name = service_data.get("name")
if name and name != service.name:
existing = db.query(Service).filter(
Service.name == name,
Service.id != id
).first()
if existing:
raise HTTPException(status_code=400, detail="Service name already exists")
# Update fields
if "name" in service_data:
service.name = service_data["name"]
if "description" in service_data:
service.description = service_data["description"]
if "price" in service_data:
service.price = float(service_data["price"])
if "category" in service_data:
service.category = service_data["category"]
if "status" in service_data:
service.is_active = service_data["status"] == "active"
db.commit()
db.refresh(service)
return {
"status": "success",
"message": "Service updated successfully",
"data": {"service": service}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_service(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete service (Admin only)"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
# Check if service is used in active bookings
active_usage = db.query(ServiceUsage).join(Booking).filter(
ServiceUsage.service_id == id,
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
if active_usage > 0:
raise HTTPException(
status_code=400,
detail="Cannot delete service that is used in active bookings"
)
db.delete(service)
db.commit()
return {
"status": "success",
"message": "Service deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/use")
async def use_service(
usage_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Add service to booking"""
try:
booking_id = usage_data.get("booking_id")
service_id = usage_data.get("service_id")
quantity = usage_data.get("quantity", 1)
# Check if booking exists
booking = db.query(Booking).filter(Booking.id == booking_id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Check if service exists and is active
service = db.query(Service).filter(Service.id == service_id).first()
if not service or not service.is_active:
raise HTTPException(status_code=404, detail="Service not found or inactive")
# Calculate total price
total_price = float(service.price) * quantity
# Create service usage
service_usage = ServiceUsage(
booking_id=booking_id,
service_id=service_id,
quantity=quantity,
unit_price=service.price,
total_price=total_price,
)
db.add(service_usage)
db.commit()
db.refresh(service_usage)
return {
"status": "success",
"message": "Service added to booking successfully",
"data": {"bookingService": service_usage}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,317 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
import bcrypt
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.role import Role
from ..models.booking import Booking, BookingStatus
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/", dependencies=[Depends(authorize_roles("admin"))])
async def get_users(
search: Optional[str] = Query(None),
role: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get all users with filters and pagination (Admin only)"""
try:
query = db.query(User)
# Filter by search (full_name, email, phone)
if search:
query = query.filter(
or_(
User.full_name.like(f"%{search}%"),
User.email.like(f"%{search}%"),
User.phone.like(f"%{search}%")
)
)
# Filter by role
if role:
role_map = {"admin": 1, "staff": 2, "customer": 3}
if role in role_map:
query = query.filter(User.role_id == role_map[role])
# Filter by status
if status_filter:
is_active = status_filter == "active"
query = query.filter(User.is_active == is_active)
# Get total count
total = query.count()
# Apply pagination
offset = (page - 1) * limit
users = query.order_by(User.created_at.desc()).offset(offset).limit(limit).all()
# Transform users
result = []
for user in users:
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone, # For frontend compatibility
"address": user.address,
"avatar": user.avatar,
"is_active": user.is_active,
"status": "active" if user.is_active else "inactive",
"role_id": user.role_id,
"role": user.role.name if user.role else "customer",
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
}
result.append(user_dict)
return {
"status": "success",
"data": {
"users": 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("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def get_user_by_id(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get user by ID (Admin only)"""
try:
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Get recent bookings
bookings = db.query(Booking).filter(
Booking.user_id == id
).order_by(Booking.created_at.desc()).limit(5).all()
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"address": user.address,
"avatar": user.avatar,
"is_active": user.is_active,
"status": "active" if user.is_active else "inactive",
"role_id": user.role_id,
"role": user.role.name if user.role else "customer",
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"bookings": [
{
"id": b.id,
"booking_number": b.booking_number,
"status": b.status.value if isinstance(b.status, BookingStatus) else b.status,
"created_at": b.created_at.isoformat() if b.created_at else None,
}
for b in bookings
],
}
return {
"status": "success",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_user(
user_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new user (Admin only)"""
try:
email = user_data.get("email")
password = user_data.get("password")
full_name = user_data.get("full_name")
phone_number = user_data.get("phone_number")
role = user_data.get("role", "customer")
status = user_data.get("status", "active")
# Map role string to role_id
role_map = {"admin": 1, "staff": 2, "customer": 3}
role_id = role_map.get(role, 3)
# Check if email exists
existing = db.query(User).filter(User.email == email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already exists")
# Hash password
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
# Create user
user = User(
email=email,
password=hashed_password,
full_name=full_name,
phone=phone_number,
role_id=role_id,
is_active=status == "active",
)
db.add(user)
db.commit()
db.refresh(user)
# Remove password from response
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"role_id": user.role_id,
"is_active": user.is_active,
}
return {
"status": "success",
"message": "User created successfully",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}")
async def update_user(
id: int,
user_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update user"""
try:
# Users can only update themselves unless they're admin
if current_user.role_id != 1 and current_user.id != id:
raise HTTPException(status_code=403, detail="Forbidden")
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check if email is being changed and if it's taken
email = user_data.get("email")
if email and email != user.email:
existing = db.query(User).filter(User.email == email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already exists")
# Map role string to role_id (only admin can change role)
role_map = {"admin": 1, "staff": 2, "customer": 3}
# Update fields
if "full_name" in user_data:
user.full_name = user_data["full_name"]
if "email" in user_data and current_user.role_id == 1:
user.email = user_data["email"]
if "phone_number" in user_data:
user.phone = user_data["phone_number"]
if "role" in user_data and current_user.role_id == 1:
user.role_id = role_map.get(user_data["role"], 3)
if "status" in user_data and current_user.role_id == 1:
user.is_active = user_data["status"] == "active"
if "password" in user_data:
password_bytes = user_data["password"].encode('utf-8')
salt = bcrypt.gensalt()
user.password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
db.commit()
db.refresh(user)
# Remove password from response
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"role_id": user.role_id,
"is_active": user.is_active,
}
return {
"status": "success",
"message": "User updated successfully",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_user(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete user (Admin only)"""
try:
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check if user has active bookings
active_bookings = db.query(Booking).filter(
Booking.user_id == id,
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
if active_bookings > 0:
raise HTTPException(
status_code=400,
detail="Cannot delete user with active bookings"
)
db.delete(user)
db.commit()
return {
"status": "success",
"message": "User deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))