This commit is contained in:
Iliyan Angelov
2025-12-04 01:07:34 +02:00
parent 5fb50983a9
commit 3d634b4fce
92 changed files with 9678 additions and 221 deletions

View File

@@ -0,0 +1,401 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import and_, or_, desc
from typing import List, Optional
from datetime import datetime
from ...shared.config.database import get_db
from ...shared.config.logging_config import get_logger
from ...security.middleware.auth import get_current_user, authorize_roles
from ...auth.models.user import User
from ...auth.models.role import Role
from ..models.guest_request import GuestRequest, RequestType, RequestStatus, RequestPriority
from ...bookings.models.booking import Booking, BookingStatus
from ...rooms.models.room import Room
from pydantic import BaseModel
logger = get_logger(__name__)
router = APIRouter(prefix='/guest-requests', tags=['guest-requests'])
# ==================== Pydantic Schemas ====================
class GuestRequestCreate(BaseModel):
booking_id: int
room_id: int
request_type: str
title: str
description: Optional[str] = None
priority: str = 'normal'
guest_notes: Optional[str] = None
class GuestRequestUpdate(BaseModel):
status: Optional[str] = None
assigned_to: Optional[int] = None
staff_notes: Optional[str] = None
# ==================== Guest Requests ====================
@router.get('/')
async def get_guest_requests(
status: Optional[str] = Query(None),
request_type: Optional[str] = Query(None),
room_id: Optional[int] = Query(None),
assigned_to: Optional[int] = Query(None),
priority: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')),
db: Session = Depends(get_db)
):
"""Get guest requests with filtering"""
try:
query = db.query(GuestRequest).options(
joinedload(GuestRequest.booking),
joinedload(GuestRequest.room),
joinedload(GuestRequest.guest)
)
# Check if user is housekeeping - they can only see requests assigned to them or unassigned
role = db.query(Role).filter(Role.id == current_user.role_id).first()
is_housekeeping = role and role.name == 'housekeeping'
if is_housekeeping:
query = query.filter(
or_(
GuestRequest.assigned_to == current_user.id,
GuestRequest.assigned_to.is_(None)
)
)
if status:
query = query.filter(GuestRequest.status == status)
if request_type:
query = query.filter(GuestRequest.request_type == request_type)
if room_id:
query = query.filter(GuestRequest.room_id == room_id)
if assigned_to:
query = query.filter(GuestRequest.assigned_to == assigned_to)
if priority:
query = query.filter(GuestRequest.priority == priority)
# Only show requests for checked-in bookings (guests must be in the room)
query = query.join(Booking).filter(
Booking.status == BookingStatus.checked_in
)
total = query.count()
requests = query.order_by(
desc(GuestRequest.priority == RequestPriority.urgent),
desc(GuestRequest.priority == RequestPriority.high),
desc(GuestRequest.requested_at)
).offset((page - 1) * limit).limit(limit).all()
return {
'status': 'success',
'data': {
'requests': [
{
'id': req.id,
'booking_id': req.booking_id,
'room_id': req.room_id,
'room_number': req.room.room_number if req.room else None,
'user_id': req.user_id,
'guest_name': req.guest.full_name if req.guest else None,
'request_type': req.request_type.value,
'status': req.status.value,
'priority': req.priority.value,
'title': req.title,
'description': req.description,
'guest_notes': req.guest_notes,
'staff_notes': req.staff_notes,
'assigned_to': req.assigned_to,
'assigned_staff_name': req.assigned_staff.full_name if req.assigned_staff else None,
'fulfilled_by': req.fulfilled_by,
'requested_at': req.requested_at.isoformat() if req.requested_at else None,
'started_at': req.started_at.isoformat() if req.started_at else None,
'fulfilled_at': req.fulfilled_at.isoformat() if req.fulfilled_at else None,
'response_time_minutes': req.response_time_minutes,
'fulfillment_time_minutes': req.fulfillment_time_minutes,
}
for req in requests
],
'pagination': {
'total': total,
'page': page,
'limit': limit,
'total_pages': (total + limit - 1) // limit
}
}
}
except Exception as e:
logger.error(f'Error fetching guest requests: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail='Failed to fetch guest requests')
@router.post('/')
async def create_guest_request(
request_data: GuestRequestCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create a new guest request"""
try:
# Verify booking belongs to user
booking = db.query(Booking).filter(Booking.id == request_data.booking_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='You can only create requests for your own bookings')
# Guests can only create requests when they are checked in (in the room)
if booking.status != BookingStatus.checked_in:
raise HTTPException(
status_code=400,
detail='You can only create requests when you are checked in. Please check in first or contact reception.'
)
# Verify room matches booking
if booking.room_id != request_data.room_id:
raise HTTPException(status_code=400, detail='Room ID does not match booking')
guest_request = GuestRequest(
booking_id=request_data.booking_id,
room_id=request_data.room_id,
user_id=current_user.id,
request_type=RequestType(request_data.request_type),
priority=RequestPriority(request_data.priority),
title=request_data.title,
description=request_data.description,
guest_notes=request_data.guest_notes,
)
db.add(guest_request)
db.commit()
db.refresh(guest_request)
return {
'status': 'success',
'message': 'Guest request created successfully',
'data': {'id': guest_request.id}
}
except ValueError as e:
raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}')
except Exception as e:
logger.error(f'Error creating guest request: {str(e)}', exc_info=True)
db.rollback()
raise HTTPException(status_code=500, detail='Failed to create guest request')
@router.put('/{request_id}')
async def update_guest_request(
request_id: int,
request_data: GuestRequestUpdate,
current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')),
db: Session = Depends(get_db)
):
"""Update a guest request (assign, update status, add notes)"""
try:
guest_request = db.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
raise HTTPException(status_code=404, detail='Guest request not found')
# Check permissions
role = db.query(Role).filter(Role.id == current_user.role_id).first()
is_housekeeping = role and role.name == 'housekeeping'
if is_housekeeping:
# Housekeeping can only update requests assigned to them or unassigned
if guest_request.assigned_to and guest_request.assigned_to != current_user.id:
raise HTTPException(status_code=403, detail='You can only update requests assigned to you')
update_data = request_data.dict(exclude_unset=True)
# Handle status changes
if 'status' in update_data:
new_status = RequestStatus(update_data['status'])
old_status = guest_request.status
# Track timestamps
if new_status == RequestStatus.in_progress and old_status == RequestStatus.pending:
guest_request.started_at = datetime.utcnow()
if guest_request.requested_at:
delta = datetime.utcnow() - guest_request.requested_at
guest_request.response_time_minutes = int(delta.total_seconds() / 60)
# Auto-assign if not assigned
if not guest_request.assigned_to:
guest_request.assigned_to = current_user.id
elif new_status == RequestStatus.fulfilled and old_status != RequestStatus.fulfilled:
guest_request.fulfilled_at = datetime.utcnow()
guest_request.fulfilled_by = current_user.id
if guest_request.started_at:
delta = datetime.utcnow() - guest_request.started_at
guest_request.fulfillment_time_minutes = int(delta.total_seconds() / 60)
update_data['status'] = new_status
if 'assigned_to' in update_data:
update_data['assigned_to'] = update_data['assigned_to'] if update_data['assigned_to'] else None
for key, value in update_data.items():
setattr(guest_request, key, value)
db.commit()
db.refresh(guest_request)
return {
'status': 'success',
'message': 'Guest request updated successfully'
}
except ValueError as e:
raise HTTPException(status_code=400, detail=f'Invalid enum value: {str(e)}')
except Exception as e:
logger.error(f'Error updating guest request: {str(e)}', exc_info=True)
db.rollback()
raise HTTPException(status_code=500, detail='Failed to update guest request')
@router.get('/{request_id}')
async def get_guest_request(
request_id: int,
current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')),
db: Session = Depends(get_db)
):
"""Get a single guest request"""
try:
request = db.query(GuestRequest).options(
joinedload(GuestRequest.booking),
joinedload(GuestRequest.room),
joinedload(GuestRequest.guest),
joinedload(GuestRequest.assigned_staff),
joinedload(GuestRequest.fulfilled_staff)
).filter(GuestRequest.id == request_id).first()
if not request:
raise HTTPException(status_code=404, detail='Guest request not found')
# Check permissions
role = db.query(Role).filter(Role.id == current_user.role_id).first()
is_housekeeping = role and role.name == 'housekeeping'
if is_housekeeping and request.assigned_to and request.assigned_to != current_user.id:
raise HTTPException(status_code=403, detail='You can only view requests assigned to you')
return {
'status': 'success',
'data': {
'id': request.id,
'booking_id': request.booking_id,
'room_id': request.room_id,
'room_number': request.room.room_number if request.room else None,
'user_id': request.user_id,
'guest_name': request.guest.full_name if request.guest else None,
'guest_email': request.guest.email if request.guest else None,
'request_type': request.request_type.value,
'status': request.status.value,
'priority': request.priority.value,
'title': request.title,
'description': request.description,
'guest_notes': request.guest_notes,
'staff_notes': request.staff_notes,
'assigned_to': request.assigned_to,
'assigned_staff_name': request.assigned_staff.full_name if request.assigned_staff else None,
'fulfilled_by': request.fulfilled_by,
'fulfilled_staff_name': request.fulfilled_staff.full_name if request.fulfilled_staff else None,
'requested_at': request.requested_at.isoformat() if request.requested_at else None,
'started_at': request.started_at.isoformat() if request.started_at else None,
'fulfilled_at': request.fulfilled_at.isoformat() if request.fulfilled_at else None,
'response_time_minutes': request.response_time_minutes,
'fulfillment_time_minutes': request.fulfillment_time_minutes,
}
}
except Exception as e:
logger.error(f'Error fetching guest request: {str(e)}', exc_info=True)
raise HTTPException(status_code=500, detail='Failed to fetch guest request')
@router.post('/{request_id}/assign')
async def assign_request(
request_id: int,
current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')),
db: Session = Depends(get_db)
):
"""Assign a request to the current user (housekeeping)"""
try:
request = db.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not request:
raise HTTPException(status_code=404, detail='Guest request not found')
if request.status == RequestStatus.fulfilled:
raise HTTPException(status_code=400, detail='Cannot assign a fulfilled request')
request.assigned_to = current_user.id
if request.status == RequestStatus.pending:
request.status = RequestStatus.in_progress
request.started_at = datetime.utcnow()
if request.requested_at:
delta = datetime.utcnow() - request.requested_at
request.response_time_minutes = int(delta.total_seconds() / 60)
db.commit()
return {
'status': 'success',
'message': 'Request assigned successfully'
}
except Exception as e:
logger.error(f'Error assigning request: {str(e)}', exc_info=True)
db.rollback()
raise HTTPException(status_code=500, detail='Failed to assign request')
@router.post('/{request_id}/fulfill')
async def fulfill_request(
request_id: int,
staff_notes: Optional[str] = None,
current_user: User = Depends(authorize_roles('admin', 'staff', 'housekeeping')),
db: Session = Depends(get_db)
):
"""Mark a request as fulfilled"""
try:
request = db.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not request:
raise HTTPException(status_code=404, detail='Guest request not found')
# Check permissions
role = db.query(Role).filter(Role.id == current_user.role_id).first()
is_housekeeping = role and role.name == 'housekeeping'
if is_housekeeping and request.assigned_to != current_user.id:
raise HTTPException(status_code=403, detail='You can only fulfill requests assigned to you')
if request.status == RequestStatus.fulfilled:
raise HTTPException(status_code=400, detail='Request is already fulfilled')
request.status = RequestStatus.fulfilled
request.fulfilled_by = current_user.id
request.fulfilled_at = datetime.utcnow()
if staff_notes:
request.staff_notes = (request.staff_notes or '') + f'\n{staff_notes}' if request.staff_notes else staff_notes
if request.started_at:
delta = datetime.utcnow() - request.started_at
request.fulfillment_time_minutes = int(delta.total_seconds() / 60)
elif request.requested_at:
# If never started, calculate from request time
delta = datetime.utcnow() - request.requested_at
request.fulfillment_time_minutes = int(delta.total_seconds() / 60)
db.commit()
return {
'status': 'success',
'message': 'Request marked as fulfilled'
}
except Exception as e:
logger.error(f'Error fulfilling request: {str(e)}', exc_info=True)
db.rollback()
raise HTTPException(status_code=500, detail='Failed to fulfill request')