This commit is contained in:
Iliyan Angelov
2025-11-30 23:29:01 +02:00
parent 39fcfff811
commit 0fa2adeb19
1058 changed files with 4630 additions and 296 deletions

View File

@@ -1,6 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from sqlalchemy.exc import IntegrityError
from typing import List, Optional
from datetime import datetime
from ...shared.config.database import get_db
@@ -9,6 +10,8 @@ from ...security.middleware.auth import get_current_user, authorize_roles
from ...auth.models.user import User
from ..models.room import Room, RoomStatus
from ..models.room_type import RoomType
from ..schemas.room import CreateRoomRequest, UpdateRoomRequest, BulkDeleteRoomsRequest
from ...shared.utils.response_helpers import success_response
from ...reviews.models.review import Review, ReviewStatus
from ...bookings.models.booking import Booking, BookingStatus
from ..services.room_service import get_rooms_with_ratings, get_amenities_list, normalize_images, get_base_url
@@ -210,22 +213,44 @@ async def get_room_by_number(room_number: str, request: Request, db: Session=Dep
raise HTTPException(status_code=500, detail=str(e))
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
async def create_room(room_data: dict, request: Request, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
async def create_room(room_data: CreateRoomRequest, request: Request, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
"""Create a new room with validated input using Pydantic schema."""
# Start transaction
transaction = db.begin()
try:
room_type = db.query(RoomType).filter(RoomType.id == room_data.get('room_type_id')).first()
# Lock room type to prevent race conditions
room_type = db.query(RoomType).filter(RoomType.id == room_data.room_type_id).with_for_update().first()
if not room_type:
transaction.rollback()
raise HTTPException(status_code=404, detail='Room type not found')
existing = db.query(Room).filter(Room.room_number == room_data.get('room_number')).first()
# Check for duplicate room number with locking
existing = db.query(Room).filter(Room.room_number == room_data.room_number).with_for_update().first()
if existing:
transaction.rollback()
raise HTTPException(status_code=400, detail='Room number already exists')
amenities_value = room_data.get('amenities', [])
if amenities_value is None:
amenities_value = []
elif not isinstance(amenities_value, list):
amenities_value = []
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), description=room_data.get('description'), capacity=room_data.get('capacity'), room_size=room_data.get('room_size'), view=room_data.get('view'), amenities=amenities_value)
# Use price from request or default to room type base price
room_price = room_data.price if room_data.price is not None else float(room_type.base_price) if room_type.base_price else 0.0
room = Room(
room_type_id=room_data.room_type_id,
room_number=room_data.room_number,
floor=room_data.floor,
status=RoomStatus(room_data.status),
featured=room_data.featured,
price=room_price,
description=room_data.description,
capacity=room_data.capacity,
room_size=room_data.room_size,
view=room_data.view,
amenities=room_data.amenities or []
)
db.add(room)
db.commit()
db.flush()
# Commit transaction
transaction.commit()
db.refresh(room)
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 is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities if room.amenities else [], 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None}
@@ -235,67 +260,118 @@ async def create_room(room_data: dict, request: Request, current_user: User=Depe
room_dict['images'] = []
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 if room.room_type.amenities else [], 'images': []}
return {'status': 'success', 'message': 'Room created successfully', 'data': {'room': room_dict}}
return success_response(data={'room': room_dict}, message='Room created successfully')
except HTTPException:
if 'transaction' in locals():
transaction.rollback()
raise
except IntegrityError as e:
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Database integrity error during room creation: {str(e)}')
raise HTTPException(status_code=409, detail='Room conflict detected. Please check room number.')
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Error creating room: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail='An error occurred while creating the room')
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
async def update_room(id: int, room_data: dict, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
async def update_room(id: int, room_data: UpdateRoomRequest, request: Request, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
"""Update a room with validated input using Pydantic schema."""
# Start transaction
transaction = db.begin()
try:
room = db.query(Room).filter(Room.id == id).first()
# Lock room row to prevent race conditions
room = db.query(Room).filter(Room.id == id).with_for_update().first()
if not room:
transaction.rollback()
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 room_data.room_type_id:
room_type = db.query(RoomType).filter(RoomType.id == room_data.room_type_id).first()
if not room_type:
transaction.rollback()
raise HTTPException(status_code=404, detail='Room type not found')
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']
if 'description' in room_data:
room.description = room_data['description']
if 'capacity' in room_data:
room.capacity = room_data['capacity']
if 'room_size' in room_data:
room.room_size = room_data['room_size']
if 'view' in room_data:
room.view = room_data['view']
if 'amenities' in room_data:
amenities_value = room_data['amenities']
if amenities_value is None:
room.amenities = []
elif isinstance(amenities_value, list):
room.amenities = amenities_value
else:
room.amenities = []
db.commit()
room.room_type_id = room_data.room_type_id
if room_data.room_number is not None:
# Check for duplicate room number
existing = db.query(Room).filter(Room.room_number == room_data.room_number, Room.id != id).first()
if existing:
transaction.rollback()
raise HTTPException(status_code=400, detail='Room number already exists')
room.room_number = room_data.room_number
if room_data.floor is not None:
room.floor = room_data.floor
if room_data.status is not None:
room.status = RoomStatus(room_data.status)
if room_data.featured is not None:
room.featured = room_data.featured
if room_data.price is not None:
room.price = room_data.price
if room_data.description is not None:
room.description = room_data.description
if room_data.capacity is not None:
room.capacity = room_data.capacity
if room_data.room_size is not None:
room.room_size = room_data.room_size
if room_data.view is not None:
room.view = room_data.view
if room_data.amenities is not None:
room.amenities = room_data.amenities or []
# Commit transaction
transaction.commit()
db.refresh(room)
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 is not None and room.price > 0 else None, 'featured': room.featured, 'description': room.description, 'capacity': room.capacity, 'room_size': room.room_size, 'view': room.view, 'amenities': room.amenities if room.amenities else [], 'created_at': room.created_at.isoformat() if room.created_at else None, 'updated_at': room.updated_at.isoformat() if room.updated_at else None}
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 is not None and room.price > 0 else None,
'featured': room.featured,
'description': room.description,
'capacity': room.capacity,
'room_size': room.room_size,
'view': room.view,
'amenities': room.amenities if room.amenities else [],
'created_at': room.created_at.isoformat() if room.created_at else None,
'updated_at': room.updated_at.isoformat() if room.updated_at else None
}
try:
room_dict['images'] = normalize_images(room.images, base_url)
except:
room_dict['images'] = []
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 if room.room_type.amenities else [], 'images': []}
return {'status': 'success', 'message': 'Room updated successfully', 'data': {'room': room_dict}}
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 if room.room_type.amenities else [],
'images': []
}
return success_response(data={'room': room_dict}, message='Room updated successfully')
except HTTPException:
if 'transaction' in locals():
transaction.rollback()
raise
except IntegrityError as e:
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Database integrity error during room update: {str(e)}')
raise HTTPException(status_code=409, detail='Room conflict detected. Please check room number.')
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Error updating room: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail='An error occurred while updating the room')
@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)):
@@ -313,30 +389,44 @@ async def delete_room(id: int, current_user: User=Depends(authorize_roles('admin
raise HTTPException(status_code=500, detail=str(e))
@router.post('/bulk-delete', dependencies=[Depends(authorize_roles('admin'))])
async def bulk_delete_rooms(room_ids: dict, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
async def bulk_delete_rooms(room_ids: BulkDeleteRoomsRequest, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
"""Bulk delete rooms with validated input using Pydantic schema."""
# Start transaction
transaction = db.begin()
try:
ids = room_ids.get('ids', [])
if not ids or not isinstance(ids, list):
raise HTTPException(status_code=400, detail='Invalid room IDs provided')
if len(ids) == 0:
raise HTTPException(status_code=400, detail='No room IDs provided')
try:
ids = [int(id) for id in ids]
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail='All room IDs must be integers')
ids = room_ids.room_ids
# Check if rooms exist
rooms = db.query(Room).filter(Room.id.in_(ids)).all()
found_ids = [room.id for room in rooms]
not_found_ids = [id for id in ids if id not in found_ids]
if not_found_ids:
transaction.rollback()
raise HTTPException(status_code=404, detail=f'Rooms with IDs {not_found_ids} not found')
# Delete rooms
deleted_count = db.query(Room).filter(Room.id.in_(ids)).delete(synchronize_session=False)
db.commit()
return {'status': 'success', 'message': f'Successfully deleted {deleted_count} room(s)', 'data': {'deleted_count': deleted_count, 'deleted_ids': ids}}
# Commit transaction
transaction.commit()
return success_response(
data={'deleted_count': deleted_count, 'deleted_ids': ids},
message=f'Successfully deleted {deleted_count} room(s)'
)
except HTTPException:
if 'transaction' in locals():
transaction.rollback()
raise
except IntegrityError as e:
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Database integrity error during bulk room deletion: {str(e)}')
raise HTTPException(status_code=409, detail='Cannot delete rooms due to existing relationships (bookings, etc.)')
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
if 'transaction' in locals():
transaction.rollback()
logger.error(f'Error bulk deleting rooms: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail='An error occurred while deleting rooms')
@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)):