update
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
116
Backend/src/guest_management/models/guest_complaint.py
Normal file
116
Backend/src/guest_management/models/guest_complaint.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Guest complaint management model.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, Enum, ForeignKey, DateTime, Boolean, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ...shared.config.database import Base
|
||||
|
||||
|
||||
class ComplaintStatus(str, enum.Enum):
|
||||
"""Complaint status enumeration."""
|
||||
open = 'open'
|
||||
in_progress = 'in_progress'
|
||||
resolved = 'resolved'
|
||||
closed = 'closed'
|
||||
escalated = 'escalated'
|
||||
|
||||
|
||||
class ComplaintPriority(str, enum.Enum):
|
||||
"""Complaint priority enumeration."""
|
||||
low = 'low'
|
||||
medium = 'medium'
|
||||
high = 'high'
|
||||
urgent = 'urgent'
|
||||
|
||||
|
||||
class ComplaintCategory(str, enum.Enum):
|
||||
"""Complaint category enumeration."""
|
||||
room_quality = 'room_quality'
|
||||
service = 'service'
|
||||
cleanliness = 'cleanliness'
|
||||
noise = 'noise'
|
||||
billing = 'billing'
|
||||
staff_behavior = 'staff_behavior'
|
||||
amenities = 'amenities'
|
||||
other = 'other'
|
||||
|
||||
|
||||
class GuestComplaint(Base):
|
||||
"""Model for guest complaints and their resolution."""
|
||||
__tablename__ = 'guest_complaints'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
|
||||
# Guest information
|
||||
guest_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True)
|
||||
booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=True, index=True)
|
||||
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True, index=True)
|
||||
|
||||
# Complaint details
|
||||
category = Column(Enum(ComplaintCategory), nullable=False, index=True)
|
||||
priority = Column(Enum(ComplaintPriority), nullable=False, default=ComplaintPriority.medium, index=True)
|
||||
status = Column(Enum(ComplaintStatus), nullable=False, default=ComplaintStatus.open, index=True)
|
||||
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
|
||||
# Resolution
|
||||
resolution = Column(Text, nullable=True)
|
||||
resolved_at = Column(DateTime, nullable=True)
|
||||
resolved_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
|
||||
# Assignment
|
||||
assigned_to = Column(Integer, ForeignKey('users.id'), nullable=True, index=True)
|
||||
escalated_to = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||
|
||||
# Tracking
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
closed_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Additional information
|
||||
guest_satisfaction_rating = Column(Integer, nullable=True) # 1-5 rating after resolution
|
||||
guest_feedback = Column(Text, nullable=True)
|
||||
internal_notes = Column(Text, nullable=True)
|
||||
attachments = Column(JSON, nullable=True) # Array of file paths/URLs
|
||||
|
||||
# Follow-up
|
||||
requires_follow_up = Column(Boolean, nullable=False, default=False)
|
||||
follow_up_date = Column(DateTime, nullable=True)
|
||||
follow_up_completed = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
# Relationships
|
||||
guest = relationship('User', foreign_keys=[guest_id])
|
||||
booking = relationship('Booking', foreign_keys=[booking_id])
|
||||
room = relationship('Room', foreign_keys=[room_id])
|
||||
assignee = relationship('User', foreign_keys=[assigned_to])
|
||||
resolver = relationship('User', foreign_keys=[resolved_by])
|
||||
escalator = relationship('User', foreign_keys=[escalated_to])
|
||||
|
||||
|
||||
class ComplaintUpdate(Base):
|
||||
"""Model for tracking complaint updates and communication."""
|
||||
__tablename__ = 'complaint_updates'
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||
complaint_id = Column(Integer, ForeignKey('guest_complaints.id'), nullable=False, index=True)
|
||||
|
||||
# Update details
|
||||
update_type = Column(String(50), nullable=False) # 'status_change', 'assignment', 'note', 'resolution'
|
||||
description = Column(Text, nullable=False)
|
||||
|
||||
# Who made the update
|
||||
updated_by = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||
|
||||
# Timestamp
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
# Additional data
|
||||
update_metadata = Column(JSON, nullable=True) # Store additional context
|
||||
|
||||
# Relationships
|
||||
complaint = relationship('GuestComplaint', backref='updates')
|
||||
updater = relationship('User', foreign_keys=[updated_by])
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
437
Backend/src/guest_management/routes/complaint_routes.py
Normal file
437
Backend/src/guest_management/routes/complaint_routes.py
Normal file
@@ -0,0 +1,437 @@
|
||||
"""
|
||||
Routes for guest complaint management.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_, or_, func
|
||||
from typing import 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 ..models.guest_complaint import (
|
||||
GuestComplaint, ComplaintStatus, ComplaintPriority, ComplaintCategory, ComplaintUpdate
|
||||
)
|
||||
from ..schemas.complaint import (
|
||||
CreateComplaintRequest, UpdateComplaintRequest,
|
||||
AddComplaintUpdateRequest, ResolveComplaintRequest
|
||||
)
|
||||
from ...shared.utils.response_helpers import success_response
|
||||
|
||||
logger = get_logger(__name__)
|
||||
router = APIRouter(prefix='/complaints', tags=['complaints'])
|
||||
|
||||
|
||||
@router.post('/')
|
||||
async def create_complaint(
|
||||
complaint_data: CreateComplaintRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Create a new guest complaint."""
|
||||
try:
|
||||
# Verify booking ownership if booking_id provided
|
||||
if complaint_data.booking_id:
|
||||
from ...bookings.models.booking import Booking
|
||||
booking = db.query(Booking).filter(Booking.id == complaint_data.booking_id).first()
|
||||
if not booking:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=404, detail='Booking not found')
|
||||
|
||||
# Check if user owns the booking (unless admin/staff)
|
||||
from ...shared.utils.role_helpers import is_admin, is_staff
|
||||
if not (is_admin(current_user, db) or is_staff(current_user, db)):
|
||||
if booking.user_id != current_user.id:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=403, detail='Access denied')
|
||||
|
||||
complaint = GuestComplaint(
|
||||
guest_id=current_user.id,
|
||||
booking_id=complaint_data.booking_id,
|
||||
room_id=complaint_data.room_id,
|
||||
category=ComplaintCategory(complaint_data.category),
|
||||
priority=ComplaintPriority(complaint_data.priority),
|
||||
status=ComplaintStatus.open,
|
||||
title=complaint_data.title,
|
||||
description=complaint_data.description,
|
||||
attachments=complaint_data.attachments or []
|
||||
)
|
||||
|
||||
db.add(complaint)
|
||||
db.flush()
|
||||
|
||||
# Create initial update
|
||||
update = ComplaintUpdate(
|
||||
complaint_id=complaint.id,
|
||||
update_type='status_change',
|
||||
description=f'Complaint created: {complaint_data.title}',
|
||||
updated_by=current_user.id,
|
||||
update_metadata={'status': 'open', 'priority': complaint_data.priority}
|
||||
)
|
||||
db.add(update)
|
||||
|
||||
db.commit()
|
||||
db.refresh(complaint)
|
||||
|
||||
return success_response(
|
||||
data={'complaint': {
|
||||
'id': complaint.id,
|
||||
'title': complaint.title,
|
||||
'status': complaint.status.value,
|
||||
'priority': complaint.priority.value,
|
||||
'category': complaint.category.value,
|
||||
'created_at': complaint.created_at.isoformat()
|
||||
}},
|
||||
message='Complaint created successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f'Error creating complaint: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while creating the complaint')
|
||||
|
||||
|
||||
@router.get('/')
|
||||
async def get_complaints(
|
||||
status: Optional[str] = Query(None),
|
||||
priority: Optional[str] = Query(None),
|
||||
category: Optional[str] = Query(None),
|
||||
assigned_to: Optional[int] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get complaints with filtering."""
|
||||
try:
|
||||
from ...shared.utils.role_helpers import is_admin, is_staff
|
||||
|
||||
query = db.query(GuestComplaint)
|
||||
|
||||
# Filter by user role
|
||||
try:
|
||||
is_admin_user = is_admin(current_user, db)
|
||||
is_staff_user = is_staff(current_user, db)
|
||||
except Exception as role_error:
|
||||
logger.warning(f'Error checking user role: {str(role_error)}')
|
||||
is_admin_user = False
|
||||
is_staff_user = False
|
||||
|
||||
if not (is_admin_user or is_staff_user):
|
||||
# Customers can only see their own complaints
|
||||
query = query.filter(GuestComplaint.guest_id == current_user.id)
|
||||
elif assigned_to:
|
||||
# Staff/admin can filter by assignee
|
||||
query = query.filter(GuestComplaint.assigned_to == assigned_to)
|
||||
|
||||
# Apply filters
|
||||
if status:
|
||||
try:
|
||||
query = query.filter(GuestComplaint.status == ComplaintStatus(status))
|
||||
except ValueError:
|
||||
logger.warning(f'Invalid status filter: {status}')
|
||||
if priority:
|
||||
try:
|
||||
query = query.filter(GuestComplaint.priority == ComplaintPriority(priority))
|
||||
except ValueError:
|
||||
logger.warning(f'Invalid priority filter: {priority}')
|
||||
if category:
|
||||
try:
|
||||
query = query.filter(GuestComplaint.category == ComplaintCategory(category))
|
||||
except ValueError:
|
||||
logger.warning(f'Invalid category filter: {category}')
|
||||
|
||||
# Pagination
|
||||
total = query.count()
|
||||
offset = (page - 1) * limit
|
||||
complaints = query.order_by(GuestComplaint.created_at.desc()).offset(offset).limit(limit).all()
|
||||
|
||||
complaints_data = []
|
||||
for complaint in complaints:
|
||||
complaints_data.append({
|
||||
'id': complaint.id,
|
||||
'title': complaint.title,
|
||||
'category': complaint.category.value,
|
||||
'priority': complaint.priority.value,
|
||||
'status': complaint.status.value,
|
||||
'guest_id': complaint.guest_id,
|
||||
'booking_id': complaint.booking_id,
|
||||
'room_id': complaint.room_id,
|
||||
'assigned_to': complaint.assigned_to,
|
||||
'created_at': complaint.created_at.isoformat(),
|
||||
'updated_at': complaint.updated_at.isoformat()
|
||||
})
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'complaints': complaints_data,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'total': total,
|
||||
'total_pages': (total + limit - 1) // limit
|
||||
}
|
||||
},
|
||||
message='Complaints retrieved successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error retrieving complaints: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=f'An error occurred while retrieving complaints: {str(e)}')
|
||||
|
||||
|
||||
@router.get('/{complaint_id}')
|
||||
async def get_complaint(
|
||||
complaint_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Get a specific complaint with details."""
|
||||
try:
|
||||
complaint = db.query(GuestComplaint).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)):
|
||||
if complaint.guest_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail='Access denied')
|
||||
|
||||
# Get updates
|
||||
updates = db.query(ComplaintUpdate).filter(
|
||||
ComplaintUpdate.complaint_id == complaint_id
|
||||
).order_by(ComplaintUpdate.created_at.asc()).all()
|
||||
|
||||
complaint_data = {
|
||||
'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,
|
||||
'booking_id': complaint.booking_id,
|
||||
'room_id': complaint.room_id,
|
||||
'assigned_to': complaint.assigned_to,
|
||||
'resolution': 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,
|
||||
'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,
|
||||
'created_at': complaint.created_at.isoformat(),
|
||||
'updated_at': complaint.updated_at.isoformat(),
|
||||
'updates': [{
|
||||
'id': u.id,
|
||||
'update_type': u.update_type,
|
||||
'description': u.description,
|
||||
'updated_by': u.updated_by,
|
||||
'created_at': u.created_at.isoformat()
|
||||
} for u in updates]
|
||||
}
|
||||
|
||||
return success_response(
|
||||
data={'complaint': complaint_data},
|
||||
message='Complaint retrieved successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f'Error retrieving complaint: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while retrieving complaint')
|
||||
|
||||
|
||||
@router.put('/{complaint_id}')
|
||||
async def update_complaint(
|
||||
complaint_id: int,
|
||||
update_data: UpdateComplaintRequest,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Update a complaint (admin/staff only)."""
|
||||
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
|
||||
changes = []
|
||||
|
||||
if update_data.status:
|
||||
old_status = complaint.status.value
|
||||
complaint.status = ComplaintStatus(update_data.status)
|
||||
changes.append(f'Status changed from {old_status} to {update_data.status}')
|
||||
|
||||
if update_data.status == 'resolved' and not complaint.resolved_at:
|
||||
complaint.resolved_at = datetime.utcnow()
|
||||
complaint.resolved_by = current_user.id
|
||||
|
||||
if update_data.priority:
|
||||
old_priority = complaint.priority.value
|
||||
complaint.priority = ComplaintPriority(update_data.priority)
|
||||
changes.append(f'Priority changed from {old_priority} to {update_data.priority}')
|
||||
|
||||
if update_data.assigned_to is not None:
|
||||
old_assignee = complaint.assigned_to
|
||||
complaint.assigned_to = update_data.assigned_to
|
||||
changes.append(f'Assigned to user {update_data.assigned_to}')
|
||||
|
||||
if update_data.resolution:
|
||||
complaint.resolution = update_data.resolution
|
||||
|
||||
if update_data.internal_notes is not None:
|
||||
complaint.internal_notes = update_data.internal_notes
|
||||
|
||||
if update_data.requires_follow_up is not None:
|
||||
complaint.requires_follow_up = update_data.requires_follow_up
|
||||
|
||||
if update_data.follow_up_date:
|
||||
complaint.follow_up_date = datetime.fromisoformat(update_data.follow_up_date.replace('Z', '+00:00'))
|
||||
|
||||
# Create update record
|
||||
if changes:
|
||||
update = ComplaintUpdate(
|
||||
complaint_id=complaint.id,
|
||||
update_type='status_change' if update_data.status else 'note',
|
||||
description='; '.join(changes),
|
||||
updated_by=current_user.id
|
||||
)
|
||||
db.add(update)
|
||||
|
||||
db.commit()
|
||||
db.refresh(complaint)
|
||||
|
||||
return success_response(
|
||||
data={'complaint': {
|
||||
'id': complaint.id,
|
||||
'status': complaint.status.value,
|
||||
'priority': complaint.priority.value,
|
||||
'assigned_to': complaint.assigned_to
|
||||
}},
|
||||
message='Complaint updated successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f'Error updating complaint: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while updating complaint')
|
||||
|
||||
|
||||
@router.post('/{complaint_id}/resolve')
|
||||
async def resolve_complaint(
|
||||
complaint_id: int,
|
||||
resolve_data: ResolveComplaintRequest,
|
||||
current_user: User = Depends(authorize_roles('admin', 'staff')),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Resolve a complaint."""
|
||||
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')
|
||||
|
||||
complaint.status = ComplaintStatus.resolved
|
||||
complaint.resolution = resolve_data.resolution
|
||||
complaint.resolved_at = datetime.utcnow()
|
||||
complaint.resolved_by = current_user.id
|
||||
|
||||
if resolve_data.guest_satisfaction_rating:
|
||||
complaint.guest_satisfaction_rating = resolve_data.guest_satisfaction_rating
|
||||
if resolve_data.guest_feedback:
|
||||
complaint.guest_feedback = resolve_data.guest_feedback
|
||||
|
||||
# Create update record
|
||||
update = ComplaintUpdate(
|
||||
complaint_id=complaint.id,
|
||||
update_type='resolution',
|
||||
description=f'Complaint resolved: {resolve_data.resolution}',
|
||||
updated_by=current_user.id,
|
||||
update_metadata={
|
||||
'satisfaction_rating': resolve_data.guest_satisfaction_rating,
|
||||
'guest_feedback': resolve_data.guest_feedback
|
||||
}
|
||||
)
|
||||
db.add(update)
|
||||
|
||||
db.commit()
|
||||
db.refresh(complaint)
|
||||
|
||||
return success_response(
|
||||
data={'complaint': {
|
||||
'id': complaint.id,
|
||||
'status': complaint.status.value,
|
||||
'resolved_at': complaint.resolved_at.isoformat()
|
||||
}},
|
||||
message='Complaint resolved successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f'Error resolving complaint: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while resolving complaint')
|
||||
|
||||
|
||||
@router.post('/{complaint_id}/updates')
|
||||
async def add_complaint_update(
|
||||
complaint_id: int,
|
||||
update_data: AddComplaintUpdateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Add an update to a complaint."""
|
||||
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')
|
||||
|
||||
# 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)):
|
||||
if complaint.guest_id != current_user.id:
|
||||
db.rollback()
|
||||
raise HTTPException(status_code=403, detail='Access denied')
|
||||
|
||||
update = ComplaintUpdate(
|
||||
complaint_id=complaint_id,
|
||||
update_type=update_data.update_type,
|
||||
description=update_data.description,
|
||||
updated_by=current_user.id,
|
||||
update_metadata=update_data.metadata or {}
|
||||
)
|
||||
|
||||
db.add(update)
|
||||
db.commit()
|
||||
db.refresh(update)
|
||||
|
||||
return success_response(
|
||||
data={'update': {
|
||||
'id': update.id,
|
||||
'update_type': update.update_type,
|
||||
'description': update.description,
|
||||
'created_at': update.created_at.isoformat()
|
||||
}},
|
||||
message='Update added successfully'
|
||||
)
|
||||
except HTTPException:
|
||||
db.rollback()
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f'Error adding complaint update: {str(e)}', exc_info=True)
|
||||
raise HTTPException(status_code=500, detail='An error occurred while adding update')
|
||||
|
||||
Binary file not shown.
Binary file not shown.
98
Backend/src/guest_management/schemas/complaint.py
Normal file
98
Backend/src/guest_management/schemas/complaint.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Pydantic schemas for guest complaint management.
|
||||
"""
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class CreateComplaintRequest(BaseModel):
|
||||
"""Schema for creating a guest complaint."""
|
||||
booking_id: Optional[int] = Field(None, gt=0, description="Related booking ID")
|
||||
room_id: Optional[int] = Field(None, gt=0, description="Related room ID")
|
||||
category: str = Field(..., description="Complaint category")
|
||||
priority: str = Field("medium", description="Complaint priority")
|
||||
title: str = Field(..., min_length=1, max_length=255, description="Complaint title")
|
||||
description: str = Field(..., min_length=1, description="Complaint description")
|
||||
attachments: Optional[List[str]] = Field(default_factory=list, description="Attachment URLs")
|
||||
|
||||
@field_validator('category')
|
||||
@classmethod
|
||||
def validate_category(cls, v: str) -> str:
|
||||
"""Validate complaint category."""
|
||||
allowed = ['room_quality', 'service', 'cleanliness', 'noise', 'billing', 'staff_behavior', 'amenities', 'other']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Category must be one of: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
@field_validator('priority')
|
||||
@classmethod
|
||||
def validate_priority(cls, v: str) -> str:
|
||||
"""Validate complaint priority."""
|
||||
allowed = ['low', 'medium', 'high', 'urgent']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Priority must be one of: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
|
||||
class UpdateComplaintRequest(BaseModel):
|
||||
"""Schema for updating a complaint."""
|
||||
status: Optional[str] = Field(None, description="New status")
|
||||
priority: Optional[str] = Field(None, description="New priority")
|
||||
assigned_to: Optional[int] = Field(None, gt=0, description="Assign to user ID")
|
||||
resolution: Optional[str] = Field(None, description="Resolution details")
|
||||
internal_notes: Optional[str] = Field(None, description="Internal notes")
|
||||
requires_follow_up: Optional[bool] = Field(None, description="Requires follow-up")
|
||||
follow_up_date: Optional[str] = Field(None, description="Follow-up date (ISO format)")
|
||||
|
||||
@field_validator('status')
|
||||
@classmethod
|
||||
def validate_status(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Validate complaint status."""
|
||||
if v:
|
||||
allowed = ['open', 'in_progress', 'resolved', 'closed', 'escalated']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Status must be one of: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
@field_validator('priority')
|
||||
@classmethod
|
||||
def validate_priority(cls, v: Optional[str]) -> Optional[str]:
|
||||
"""Validate complaint priority."""
|
||||
if v:
|
||||
allowed = ['low', 'medium', 'high', 'urgent']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Priority must be one of: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
|
||||
class AddComplaintUpdateRequest(BaseModel):
|
||||
"""Schema for adding an update to a complaint."""
|
||||
update_type: str = Field(..., description="Type of update")
|
||||
description: str = Field(..., min_length=1, description="Update description")
|
||||
metadata: Optional[dict] = Field(default_factory=dict, description="Additional metadata")
|
||||
|
||||
@field_validator('update_type')
|
||||
@classmethod
|
||||
def validate_update_type(cls, v: str) -> str:
|
||||
"""Validate update type."""
|
||||
allowed = ['status_change', 'assignment', 'note', 'resolution', 'escalation']
|
||||
if v not in allowed:
|
||||
raise ValueError(f'Update type must be one of: {", ".join(allowed)}')
|
||||
return v
|
||||
|
||||
|
||||
class ResolveComplaintRequest(BaseModel):
|
||||
"""Schema for resolving a complaint."""
|
||||
resolution: str = Field(..., min_length=1, description="Resolution details")
|
||||
guest_satisfaction_rating: Optional[int] = Field(None, ge=1, le=5, description="Guest satisfaction rating (1-5)")
|
||||
guest_feedback: Optional[str] = Field(None, description="Guest feedback on resolution")
|
||||
|
||||
@field_validator('guest_satisfaction_rating')
|
||||
@classmethod
|
||||
def validate_rating(cls, v: Optional[int]) -> Optional[int]:
|
||||
"""Validate rating."""
|
||||
if v is not None and (v < 1 or v > 5):
|
||||
raise ValueError('Rating must be between 1 and 5')
|
||||
return v
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user