Files
admin-edu-space/backend/app/services/reservation_service.py
T

123 lines
5.9 KiB
Python

from typing import Optional, List
from datetime import datetime
from app.models.reservation import Reservation, ReservationStatus
from app.models.classroom import Classroom
from app.repositories.reservation_repository import ReservationRepository
from app.repositories.classroom_repository import ClassroomRepository
from app.schemas.reservation_dto import ReservationCreateDTO, ReservationUpdateDTO
from app import db
class ReservationService:
"""Capa de servicios para la orquestación y validación de reservas y cronograma."""
def __init__(self, reservation_repo: Optional[ReservationRepository] = None,
classroom_repo: Optional[ClassroomRepository] = None):
self.reservation_repo = reservation_repo or ReservationRepository()
self.classroom_repo = classroom_repo or ClassroomRepository()
def get_reservation(self, reservation_id: int) -> Optional[Reservation]:
"""Obtiene una reserva por su ID."""
return self.reservation_repo.get_by_id(reservation_id)
def list_reservations(self, start_dt: datetime, end_dt: datetime,
classroom_id: Optional[int] = None) -> List[Reservation]:
"""Obtiene reservas dentro de un rango temporal."""
return self.reservation_repo.get_by_date_range(start_dt, end_dt, classroom_id=classroom_id)
def check_conflicts(self, classroom_id: int, start_time: datetime, end_time: datetime,
exclude_id: Optional[int] = None) -> List[Reservation]:
"""Verifica si existen solapamientos en un aula específica."""
classroom = self.classroom_repo.get_by_id(classroom_id)
# Las aulas virtuales tienen concurrencia ilimitada (sin colisiones)
if classroom and classroom.is_virtual:
return []
return self.reservation_repo.find_conflicts(
classroom_id=classroom_id,
start_time=start_time,
end_time=end_time,
exclude_reservation_id=exclude_id
)
def create_reservation(self, dto: ReservationCreateDTO) -> Reservation:
"""
Crea una nueva reserva aplicando validaciones de aforo y detección de colisiones.
"""
classroom = self.classroom_repo.get_by_id(dto.classroom_id)
if not classroom or not classroom.is_active:
raise ValueError(f"El aula con ID {dto.classroom_id} no existe o no se encuentra activa.")
# Detección de colisiones para aulas físicas
if not classroom.is_virtual:
conflicts = self.check_conflicts(dto.classroom_id, dto.start_time, dto.end_time)
if conflicts:
conflict_details = ", ".join([f"#{c.id} ({c.start_time.strftime('%H:%M')} a {c.end_time.strftime('%H:%M')})" for c in conflicts])
raise ValueError(f"Conflicto de horario en {classroom.code}: se solapa con las reservas {conflict_details}.")
if dto.expected_attendees > classroom.capacity:
raise ValueError(f"La cantidad de alumnos ({dto.expected_attendees}) supera la capacidad del aula ({classroom.capacity}).")
reservation = Reservation(
classroom_id=dto.classroom_id,
commission_id=dto.commission_id,
user_id=dto.user_id,
start_time=dto.start_time,
end_time=dto.end_time,
purpose=dto.purpose,
expected_attendees=dto.expected_attendees,
shift=dto.shift,
virtual_link=dto.virtual_link,
notes=dto.notes,
status=dto.status or 'CONFIRMED'
)
return self.reservation_repo.save(reservation)
def update_reservation(self, reservation_id: int, dto: ReservationUpdateDTO) -> Reservation:
"""Actualiza una reserva existente verificando disponibilidad horaria."""
reservation = self.reservation_repo.get_by_id(reservation_id)
if not reservation:
raise ValueError(f"Reserva con ID {reservation_id} no encontrada.")
classroom_id = dto.classroom_id or reservation.classroom_id
start_time = dto.start_time or reservation.start_time
end_time = dto.end_time or reservation.end_time
classroom = self.classroom_repo.get_by_id(classroom_id)
if not classroom or not classroom.is_active:
raise ValueError(f"El aula con ID {classroom_id} no existe o no se encuentra activa.")
if not classroom.is_virtual:
conflicts = self.check_conflicts(classroom_id, start_time, end_time, exclude_id=reservation.id)
if conflicts:
conflict_details = ", ".join([f"#{c.id} ({c.start_time.strftime('%H:%M')} - {c.end_time.strftime('%H:%M')})" for c in conflicts])
raise ValueError(f"Conflicto de horario en {classroom.code} con: {conflict_details}.")
if dto.classroom_id is not None:
reservation.classroom_id = dto.classroom_id
if dto.start_time is not None:
reservation.start_time = dto.start_time
if dto.end_time is not None:
reservation.end_time = dto.end_time
if dto.purpose is not None:
reservation.purpose = dto.purpose
if dto.expected_attendees is not None:
reservation.expected_attendees = dto.expected_attendees
if dto.shift is not None:
reservation.shift = dto.shift
if dto.virtual_link is not None:
reservation.virtual_link = dto.virtual_link
if dto.notes is not None:
reservation.notes = dto.notes
if dto.status is not None:
reservation.status = dto.status
return self.reservation_repo.save(reservation)
def cancel_reservation(self, reservation_id: int) -> bool:
"""Cancela una reserva estableciendo su estado en CANCELLED."""
reservation = self.reservation_repo.get_by_id(reservation_id)
if not reservation:
return False
reservation.status = ReservationStatus.CANCELLED.value
self.reservation_repo.save(reservation)
return True