updates
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Routes for guest complaint management.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
from typing import Optional
|
||||
@@ -18,6 +18,7 @@ from ..schemas.complaint import (
|
||||
AddComplaintUpdateRequest, ResolveComplaintRequest
|
||||
)
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
from ...analytics.services.audit_service import audit_service
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/complaints', tags=['complaints'])
|
||||
@@ -26,10 +27,15 @@ router = APIRouter(prefix='/complaints', tags=['complaints'])
|
||||
@router.post('/')
|
||||
async def create_complaint(
|
||||
complaint_data: CreateComplaintRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new guest complaint."""
|
||||
client_ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
request_id = getattr(request.state, 'request_id', None)
|
||||
|
||||
try:
|
||||
# Verify booking ownership if booking_id provided
|
||||
if complaint_data.booking_id:
|
||||
@@ -74,6 +80,31 @@ async def create_complaint(
|
||||
db.commit()
|
||||
db.refresh(complaint)
|
||||
|
||||
# SECURITY: Log complaint creation for audit trail
|
||||
try:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='complaint_created',
|
||||
resource_type='complaint',
|
||||
user_id=current_user.id,
|
||||
resource_id=complaint.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={
|
||||
'complaint_id': complaint.id,
|
||||
'title': complaint.title,
|
||||
'category': complaint.category.value,
|
||||
'priority': complaint.priority.value,
|
||||
'status': complaint.status.value,
|
||||
'booking_id': complaint.booking_id,
|
||||
'room_id': complaint.room_id
|
||||
},
|
||||
status='success'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to log complaint creation audit: {e}')
|
||||
|
||||
return success_response(
|
||||
data={'complaint': {
|
||||
'id': complaint.id,
|
||||
@@ -127,6 +158,15 @@ async def get_complaints(
|
||||
# Staff/admin can filter by assignee
|
||||
query = query.filter(GuestComplaint.assigned_to == assigned_to)
|
||||
|
||||
from sqlalchemy.orm import joinedload
|
||||
from ...rooms.models.room import Room
|
||||
|
||||
# Eager load relationships
|
||||
query = query.options(
|
||||
joinedload(GuestComplaint.guest),
|
||||
joinedload(GuestComplaint.assignee)
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
try:
|
||||
@@ -151,16 +191,29 @@ async def get_complaints(
|
||||
|
||||
complaints_data = []
|
||||
for complaint in complaints:
|
||||
# Get room number if room_id exists
|
||||
room_number = None
|
||||
if complaint.room_id:
|
||||
room = db.query(Room).filter(Room.id == complaint.room_id).first()
|
||||
if room:
|
||||
room_number = room.room_number
|
||||
|
||||
complaints_data.append({
|
||||
'id': complaint.id,
|
||||
'title': complaint.title,
|
||||
'description': complaint.description,
|
||||
'category': complaint.category.value,
|
||||
'priority': complaint.priority.value,
|
||||
'status': complaint.status.value,
|
||||
'guest_id': complaint.guest_id,
|
||||
'guest_name': complaint.guest.full_name if complaint.guest else None,
|
||||
'booking_id': complaint.booking_id,
|
||||
'room_id': complaint.room_id,
|
||||
'room_number': room_number,
|
||||
'assigned_to': complaint.assigned_to,
|
||||
'assigned_staff_name': complaint.assignee.full_name if complaint.assignee else None,
|
||||
'resolution_notes': complaint.resolution,
|
||||
'resolved_at': complaint.resolved_at.isoformat() if complaint.resolved_at else None,
|
||||
'created_at': complaint.created_at.isoformat(),
|
||||
'updated_at': complaint.updated_at.isoformat()
|
||||
})
|
||||
@@ -192,21 +245,37 @@ async def get_complaint(
|
||||
):
|
||||
"""Get a specific complaint with details."""
|
||||
try:
|
||||
complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first()
|
||||
from sqlalchemy.orm import joinedload
|
||||
from ...rooms.models.room import Room
|
||||
|
||||
complaint = db.query(GuestComplaint).options(
|
||||
joinedload(GuestComplaint.guest),
|
||||
joinedload(GuestComplaint.assignee),
|
||||
joinedload(GuestComplaint.room)
|
||||
).filter(GuestComplaint.id == complaint_id).first()
|
||||
if not complaint:
|
||||
raise HTTPException(status_code=404, detail='Complaint not found')
|
||||
|
||||
# Check access
|
||||
from ...shared.utils.role_helpers import is_admin, is_staff
|
||||
if not (is_admin(current_user, db) or is_staff(current_user, db)):
|
||||
is_admin_user = is_admin(current_user, db)
|
||||
is_staff_user = is_staff(current_user, db)
|
||||
if not (is_admin_user or is_staff_user):
|
||||
if complaint.guest_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Access denied')
|
||||
|
||||
# Get updates
|
||||
updates = db.query(ComplaintUpdate).filter(
|
||||
updates = db.query(ComplaintUpdate).options(
|
||||
joinedload(ComplaintUpdate.updater)
|
||||
).filter(
|
||||
ComplaintUpdate.complaint_id == complaint_id
|
||||
).order_by(ComplaintUpdate.created_at.asc()).all()
|
||||
|
||||
# Get room number
|
||||
room_number = None
|
||||
if complaint.room:
|
||||
room_number = complaint.room.room_number
|
||||
|
||||
complaint_data = {
|
||||
'id': complaint.id,
|
||||
'title': complaint.title,
|
||||
@@ -215,15 +284,18 @@ async def get_complaint(
|
||||
'priority': complaint.priority.value,
|
||||
'status': complaint.status.value,
|
||||
'guest_id': complaint.guest_id,
|
||||
'guest_name': complaint.guest.full_name if complaint.guest else None,
|
||||
'booking_id': complaint.booking_id,
|
||||
'room_id': complaint.room_id,
|
||||
'room_number': room_number,
|
||||
'assigned_to': complaint.assigned_to,
|
||||
'resolution': complaint.resolution,
|
||||
'assigned_staff_name': complaint.assignee.full_name if complaint.assignee else None,
|
||||
'resolution_notes': complaint.resolution,
|
||||
'resolved_at': complaint.resolved_at.isoformat() if complaint.resolved_at else None,
|
||||
'resolved_by': complaint.resolved_by,
|
||||
'guest_satisfaction_rating': complaint.guest_satisfaction_rating,
|
||||
'guest_feedback': complaint.guest_feedback,
|
||||
'internal_notes': complaint.internal_notes if (is_admin(current_user, db) or is_staff(current_user, db)) else None,
|
||||
'internal_notes': complaint.internal_notes if (is_admin_user or is_staff_user) else None,
|
||||
'attachments': complaint.attachments,
|
||||
'requires_follow_up': complaint.requires_follow_up,
|
||||
'follow_up_date': complaint.follow_up_date.isoformat() if complaint.follow_up_date else None,
|
||||
@@ -234,6 +306,7 @@ async def get_complaint(
|
||||
'update_type': u.update_type,
|
||||
'description': u.description,
|
||||
'updated_by': u.updated_by,
|
||||
'updated_by_name': u.updater.full_name if u.updater else None,
|
||||
'created_at': u.created_at.isoformat()
|
||||
} for u in updates]
|
||||
}
|
||||
@@ -253,17 +326,27 @@ async def get_complaint(
|
||||
async def update_complaint(
|
||||
complaint_id: int,
|
||||
update_data: UpdateComplaintRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a complaint (admin/staff only)."""
|
||||
client_ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
request_id = getattr(request.state, 'request_id', None)
|
||||
|
||||
try:
|
||||
complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first()
|
||||
if not complaint:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=404, detail='Complaint not found')
|
||||
|
||||
# Track changes
|
||||
# Track changes for audit
|
||||
old_values = {
|
||||
'status': complaint.status.value,
|
||||
'priority': complaint.priority.value,
|
||||
'assigned_to': complaint.assigned_to
|
||||
}
|
||||
changes = []
|
||||
|
||||
if update_data.status:
|
||||
@@ -308,6 +391,53 @@ async def update_complaint(
|
||||
db.add(update)
|
||||
|
||||
db.commit()
|
||||
|
||||
# SECURITY: Log complaint status change for audit trail
|
||||
if update_data.status and old_values['status'] != update_data.status:
|
||||
try:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='complaint_status_changed',
|
||||
resource_type='complaint',
|
||||
user_id=current_user.id,
|
||||
resource_id=complaint.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={
|
||||
'complaint_id': complaint.id,
|
||||
'old_status': old_values['status'],
|
||||
'new_status': update_data.status,
|
||||
'guest_id': complaint.guest_id,
|
||||
'resolved_by': current_user.id if update_data.status == 'resolved' else None
|
||||
},
|
||||
status='success'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to log complaint status change audit: {e}')
|
||||
|
||||
# SECURITY: Log complaint assignment change for audit trail
|
||||
if update_data.assigned_to is not None and old_values['assigned_to'] != update_data.assigned_to:
|
||||
try:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='complaint_assigned',
|
||||
resource_type='complaint',
|
||||
user_id=current_user.id,
|
||||
resource_id=complaint.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={
|
||||
'complaint_id': complaint.id,
|
||||
'old_assigned_to': old_values['assigned_to'],
|
||||
'new_assigned_to': update_data.assigned_to,
|
||||
'guest_id': complaint.guest_id
|
||||
},
|
||||
status='success'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to log complaint assignment audit: {e}')
|
||||
db.refresh(complaint)
|
||||
|
||||
return success_response(
|
||||
@@ -332,16 +462,23 @@ async def update_complaint(
|
||||
async def resolve_complaint(
|
||||
complaint_id: int,
|
||||
resolve_data: ResolveComplaintRequest,
|
||||
request: Request,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Resolve a complaint."""
|
||||
client_ip = request.client.host if request.client else None
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
request_id = getattr(request.state, 'request_id', None)
|
||||
|
||||
try:
|
||||
complaint = db.query(GuestComplaint).filter(GuestComplaint.id == complaint_id).first()
|
||||
if not complaint:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=404, detail='Complaint not found')
|
||||
|
||||
old_status = complaint.status.value
|
||||
|
||||
complaint.status = ComplaintStatus.resolved
|
||||
complaint.resolution = resolve_data.resolution
|
||||
complaint.resolved_at = datetime.utcnow()
|
||||
@@ -366,6 +503,32 @@ async def resolve_complaint(
|
||||
db.add(update)
|
||||
|
||||
db.commit()
|
||||
|
||||
# SECURITY: Log complaint resolution for audit trail
|
||||
try:
|
||||
await audit_service.log_action(
|
||||
db=db,
|
||||
action='complaint_resolved',
|
||||
resource_type='complaint',
|
||||
user_id=current_user.id,
|
||||
resource_id=complaint.id,
|
||||
ip_address=client_ip,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details={
|
||||
'complaint_id': complaint.id,
|
||||
'old_status': old_status,
|
||||
'new_status': 'resolved',
|
||||
'guest_id': complaint.guest_id,
|
||||
'resolved_by': current_user.id,
|
||||
'satisfaction_rating': resolve_data.guest_satisfaction_rating,
|
||||
'has_feedback': bool(resolve_data.guest_feedback)
|
||||
},
|
||||
status='success'
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to log complaint resolution audit: {e}')
|
||||
|
||||
db.refresh(complaint)
|
||||
|
||||
return success_response(
|
||||
|
||||
Reference in New Issue
Block a user