diff --git a/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/ReservaRestController.java b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/ReservaRestController.java index 1f379b1..674e569 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/ReservaRestController.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/ReservaRestController.java @@ -1,9 +1,13 @@ package com.aponia.aponia_hotel.controller.reservas; +import com.aponia.aponia_hotel.controller.reservas.dto.ReservaHabitacionRequest; +import com.aponia.aponia_hotel.controller.reservas.dto.ReservaHabitacionResponse; import com.aponia.aponia_hotel.entities.reservas.Reserva; import com.aponia.aponia_hotel.service.reservas.ReservaService; import io.swagger.v3.oas.annotations.Operation; import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -73,6 +77,23 @@ public double total(@PathVariable String id) { return service.calcularTotalReserva(id); } + @PostMapping("/cliente/{clienteId}/habitaciones") + @Operation(summary = "Permite a un cliente crear una reserva para un tipo de habitación") + public ResponseEntity reservarHabitacion( + @PathVariable String clienteId, + @RequestBody ReservaHabitacionRequest request) { + Reserva reserva = service.crearReservaCliente( + clienteId, + request.tipoHabitacionId(), + request.entrada(), + request.salida(), + request.numeroHuespedes(), + request.notas() + ); + return ResponseEntity.status(HttpStatus.CREATED) + .body(ReservaHabitacionResponse.fromReserva(reserva)); + } + // ===== Mutaciones ===== @PostMapping("/add") diff --git a/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionRequest.java b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionRequest.java new file mode 100644 index 0000000..9dd5f36 --- /dev/null +++ b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionRequest.java @@ -0,0 +1,11 @@ +package com.aponia.aponia_hotel.controller.reservas.dto; + +import java.time.LocalDate; + +public record ReservaHabitacionRequest( + String tipoHabitacionId, + LocalDate entrada, + LocalDate salida, + Integer numeroHuespedes, + String notas +) {} diff --git a/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionResponse.java b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionResponse.java new file mode 100644 index 0000000..212fc43 --- /dev/null +++ b/backend/src/main/java/com/aponia/aponia_hotel/controller/reservas/dto/ReservaHabitacionResponse.java @@ -0,0 +1,56 @@ +package com.aponia.aponia_hotel.controller.reservas.dto; + +import com.aponia.aponia_hotel.entities.reservas.Estancia; +import com.aponia.aponia_hotel.entities.reservas.Reserva; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +public record ReservaHabitacionResponse( + String reservaId, + String codigo, + LocalDateTime fechaCreacion, + Reserva.EstadoReserva estado, + LocalDate entrada, + LocalDate salida, + Integer numeroHuespedes, + String tipoHabitacionId, + String tipoHabitacionNombre, + BigDecimal precioPorNoche, + BigDecimal totalEstadia, + BigDecimal totalReserva +) { + public static ReservaHabitacionResponse fromReserva(Reserva reserva) { + if (reserva == null) { + throw new IllegalArgumentException("La reserva no puede ser nula"); + } + List estancias = reserva.getEstancias(); + if (estancias == null || estancias.isEmpty()) { + throw new IllegalArgumentException("La reserva no contiene estancias asociadas"); + } + Estancia estancia = estancias.get(0); + BigDecimal totalReserva = null; + if (reserva.getResumenPago() != null && reserva.getResumenPago().getTotalReserva() != null) { + totalReserva = reserva.getResumenPago().getTotalReserva(); + } else if (estancia.getTotalEstadia() != null) { + totalReserva = estancia.getTotalEstadia(); + } + + return new ReservaHabitacionResponse( + reserva.getId(), + reserva.getCodigo(), + reserva.getFechaCreacion(), + reserva.getEstado(), + estancia.getEntrada(), + estancia.getSalida(), + estancia.getNumeroHuespedes(), + estancia.getTipoHabitacion() != null ? estancia.getTipoHabitacion().getId() : null, + estancia.getTipoHabitacion() != null ? estancia.getTipoHabitacion().getNombre() : null, + estancia.getPrecioPorNoche(), + estancia.getTotalEstadia(), + totalReserva + ); + } +} diff --git a/backend/src/main/java/com/aponia/aponia_hotel/entities/pagos/ResumenPago.java b/backend/src/main/java/com/aponia/aponia_hotel/entities/pagos/ResumenPago.java index 1979923..ef872f2 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/entities/pagos/ResumenPago.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/entities/pagos/ResumenPago.java @@ -48,6 +48,13 @@ public class ResumenPago { @Column(name = "ultima_actualizacion", nullable = false) private LocalDateTime ultimaActualizacion; + public void setReserva(Reserva reserva) { + this.reserva = reserva; + if (reserva != null) { + this.reservaId = reserva.getId(); + } + } + @PrePersist @PreUpdate public void validate() { diff --git a/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Estancia.java b/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Estancia.java index c566232..4da0274 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Estancia.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Estancia.java @@ -26,10 +26,10 @@ public class Estancia { private String id; @Column(name = "check_in", nullable = false) - private Boolean checkIn; + private Boolean checkIn = Boolean.FALSE; @Column(name = "check_out", nullable = false) - private Boolean checkOut; + private Boolean checkOut = Boolean.FALSE; @Column(name = "entrada", nullable = false) private LocalDate entrada; @@ -83,6 +83,7 @@ public void validate() { throw new IllegalStateException("El total de la estadía debe ser no negativo"); } if (tipoHabitacion == null) { + throw new IllegalStateException("El tipo de habitación es requerido"); } } } \ No newline at end of file diff --git a/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Reserva.java b/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Reserva.java index 47b6001..f4cb798 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Reserva.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/entities/reservas/Reserva.java @@ -11,6 +11,7 @@ import lombok.AllArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.time.LocalDateTime; import java.util.List; @@ -32,6 +33,7 @@ public class Reserva { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "cliente_id", nullable = false) + @JsonIgnoreProperties({"passwordHash", "empleadoPerfil", "clientePerfil", "hibernateLazyInitializer", "handler"}) private Usuario cliente; @CreationTimestamp @@ -57,7 +59,7 @@ public class Reserva { @JsonIgnore private List pagos; - @OneToOne(mappedBy = "reserva", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @OneToOne(mappedBy = "reserva", fetch = FetchType.LAZY) @JsonIgnore private ResumenPago resumenPago; diff --git a/backend/src/main/java/com/aponia/aponia_hotel/repository/habitaciones/HabitacionRepository.java b/backend/src/main/java/com/aponia/aponia_hotel/repository/habitaciones/HabitacionRepository.java index 27e8099..9becbd0 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/repository/habitaciones/HabitacionRepository.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/repository/habitaciones/HabitacionRepository.java @@ -25,7 +25,7 @@ AND NOT EXISTS ( FROM Estancia e WHERE e.habitacionAsignada.id = h.id AND e.reserva.estado = 'CONFIRMADA' - AND (e.salida <= :entrada AND e.salida >= :entrada) + AND (e.entrada < :salida AND e.salida > :entrada) ) ORDER BY h.numero """) diff --git a/backend/src/main/java/com/aponia/aponia_hotel/repository/reservas/EstanciaRepository.java b/backend/src/main/java/com/aponia/aponia_hotel/repository/reservas/EstanciaRepository.java index f387fd7..40d80b0 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/repository/reservas/EstanciaRepository.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/repository/reservas/EstanciaRepository.java @@ -15,7 +15,7 @@ public interface EstanciaRepository extends JpaRepository { @Query("SELECT COUNT(DISTINCT e.habitacionAsignada) FROM Estancia e " + "WHERE e.tipoHabitacion.id = :tipoHabitacionId " + "AND e.reserva.estado = 'CONFIRMADA' " + - "AND ((e.checkIn <= :checkOut AND e.checkOut >= :checkIn))") + "AND (e.entrada < :checkOut AND e.salida > :checkIn)") long contarHabitacionesOcupadas( @Param("tipoHabitacionId") String tipoHabitacionId, @Param("checkIn") LocalDate checkIn, @@ -24,7 +24,7 @@ long contarHabitacionesOcupadas( @Query("SELECT e FROM Estancia e " + "WHERE e.habitacionAsignada.id = :habitacionId " + "AND e.reserva.estado = 'CONFIRMADA' " + - "AND ((e.checkIn <= :checkOut AND e.checkOut >= :checkIn))") + "AND (e.entrada < :checkOut AND e.salida > :checkIn)") List findOverlappingStays( @Param("habitacionId") String habitacionId, @Param("checkIn") LocalDate checkIn, @@ -33,7 +33,7 @@ List findOverlappingStays( @Query("SELECT e FROM Estancia e " + "WHERE e.tipoHabitacion.id = :tipoHabitacionId " + "AND e.reserva.estado = 'CONFIRMADA' " + - "AND e.checkIn = :fecha") + "AND e.entrada = :fecha") List findCheckinsByTipoHabitacionAndFecha( @Param("tipoHabitacionId") String tipoHabitacionId, @Param("fecha") LocalDate fecha); @@ -41,7 +41,7 @@ List findCheckinsByTipoHabitacionAndFecha( @Query("SELECT e FROM Estancia e " + "WHERE e.tipoHabitacion.id = :tipoHabitacionId " + "AND e.reserva.estado = 'CONFIRMADA' " + - "AND e.checkOut = :fecha") + "AND e.salida = :fecha") List findCheckoutsByTipoHabitacionAndFecha( @Param("tipoHabitacionId") String tipoHabitacionId, @Param("fecha") LocalDate fecha); diff --git a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/EstanciaServiceImpl.java b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/EstanciaServiceImpl.java index 97dac3c..2ab631a 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/EstanciaServiceImpl.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/EstanciaServiceImpl.java @@ -141,8 +141,8 @@ public List buscarConflictosFechas(String habitacionId, LocalDate chec } private void validarEstancia(Estancia estancia) { - if (estancia.getCheckIn() == null || estancia.getCheckOut() == null) { - throw new IllegalArgumentException("Las fechas de check-in y check-out son requeridas"); + if (estancia.getEntrada() == null || estancia.getSalida() == null) { + throw new IllegalArgumentException("Las fechas de entrada y salida son requeridas"); } if (!estancia.getSalida().isAfter(estancia.getEntrada())) { throw new IllegalArgumentException("La fecha de check-out debe ser posterior a la de check-in"); diff --git a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaService.java b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaService.java index d94637a..68cf459 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaService.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaService.java @@ -22,4 +22,5 @@ public interface ReservaService { public void completarReserva(String id); public boolean verificarDisponibilidad(String tipoHabitacionId, LocalDate entrada, LocalDate salida, int numeroHuespedes); public double calcularTotalReserva(String id); + Reserva crearReservaCliente(String clienteId, String tipoHabitacionId, LocalDate entrada, LocalDate salida, Integer numeroHuespedes, String notas); } \ No newline at end of file diff --git a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaServiceImpl.java b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaServiceImpl.java index 253c276..1c96a24 100644 --- a/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaServiceImpl.java +++ b/backend/src/main/java/com/aponia/aponia_hotel/service/reservas/ReservaServiceImpl.java @@ -5,17 +5,24 @@ import com.aponia.aponia_hotel.entities.reservas.Estancia; import com.aponia.aponia_hotel.entities.reservas.Reserva; import com.aponia.aponia_hotel.entities.reservas.Reserva.EstadoReserva; +import com.aponia.aponia_hotel.entities.reservas.ReservaServicio; +import com.aponia.aponia_hotel.entities.usuarios.Usuario; import com.aponia.aponia_hotel.repository.habitaciones.HabitacionTipoRepository; import com.aponia.aponia_hotel.repository.reservas.EstanciaRepository; import com.aponia.aponia_hotel.repository.reservas.ReservaRepository; import com.aponia.aponia_hotel.repository.pagos.ResumenPagoRepository; +import com.aponia.aponia_hotel.repository.usuarios.UsuarioRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.Objects; +import java.util.UUID; @Service @Transactional @@ -25,16 +32,19 @@ public class ReservaServiceImpl implements ReservaService { private final EstanciaRepository estanciaRepository; private final HabitacionTipoRepository habitacionTipoRepository; private final ResumenPagoRepository resumenPagoRepository; + private final UsuarioRepository usuarioRepository; public ReservaServiceImpl( ReservaRepository repository, EstanciaRepository estanciaRepository, HabitacionTipoRepository habitacionTipoRepository, - ResumenPagoRepository resumenPagoRepository) { + ResumenPagoRepository resumenPagoRepository, + UsuarioRepository usuarioRepository) { this.repository = repository; this.estanciaRepository = estanciaRepository; this.habitacionTipoRepository = habitacionTipoRepository; this.resumenPagoRepository = resumenPagoRepository; + this.usuarioRepository = usuarioRepository; } @Override @@ -67,17 +77,17 @@ public List listarReservasActivas(String clienteId) { @Override public Reserva crear(Reserva reserva) { validarReserva(reserva); + if (reserva.getId() == null || reserva.getId().isBlank()) { + reserva.setId(UUID.randomUUID().toString()); + } if (repository.existsByCodigo(reserva.getCodigo())) { throw new IllegalArgumentException("Ya existe una reserva con ese código"); } reserva.setEstado(EstadoReserva.PENDIENTE); - Reserva nuevaReserva = repository.save(reserva); + Reserva nuevaReserva = repository.saveAndFlush(reserva); - // Inicializar el resumen de pagos - ResumenPago resumen = new ResumenPago(); - resumen.setReserva(nuevaReserva); - resumenPagoRepository.save(resumen); + cargarResumenPago(nuevaReserva); return nuevaReserva; } @@ -166,15 +176,16 @@ public void completarReserva(String id) { @Override public boolean verificarDisponibilidad(String tipoHabitacionId, LocalDate entrada, LocalDate salida, int numeroHuespedes) { + String tipoId = validarIdentificador(tipoHabitacionId, "del tipo de habitación"); // Verificar que el tipo de habitación existe y tiene capacidad suficiente - Optional tipo = habitacionTipoRepository.findById(tipoHabitacionId); + Optional tipo = habitacionTipoRepository.findById(tipoId); if (tipo.isEmpty() || !tipo.get().getActiva() || tipo.get().getAforoMaximo() < numeroHuespedes) { return false; } // Contar cuántas habitaciones de este tipo están ocupadas en las fechas solicitadas long habitacionesOcupadas = estanciaRepository.contarHabitacionesOcupadas( - tipoHabitacionId, entrada, salida); + tipoId, entrada, salida); // Contar el total de habitaciones de este tipo long totalHabitaciones = tipo.get().getHabitaciones().stream() @@ -184,11 +195,121 @@ public boolean verificarDisponibilidad(String tipoHabitacionId, LocalDate entrad return habitacionesOcupadas < totalHabitaciones; } + @Override + public Reserva crearReservaCliente(String clienteId, String tipoHabitacionId, LocalDate entrada, LocalDate salida, Integer numeroHuespedes, String notas) { + Objects.requireNonNull(entrada, "La fecha de entrada es requerida"); + Objects.requireNonNull(salida, "La fecha de salida es requerida"); + Objects.requireNonNull(numeroHuespedes, "El número de huéspedes es requerido"); + + if (!salida.isAfter(entrada)) { + throw new IllegalArgumentException("La fecha de salida debe ser posterior a la de entrada"); + } + if (numeroHuespedes <= 0) { + throw new IllegalArgumentException("El número de huéspedes debe ser positivo"); + } + + String clienteIdNormalizado = validarIdentificador(clienteId, "del cliente"); + Usuario cliente = usuarioRepository.findById(clienteIdNormalizado) + .orElseThrow(() -> new IllegalArgumentException("No se encontró el cliente indicado")); + + String tipoHabitacionIdNormalizado = validarIdentificador(tipoHabitacionId, "del tipo de habitación"); + HabitacionTipo tipoHabitacion = habitacionTipoRepository.findById(tipoHabitacionIdNormalizado) + .orElseThrow(() -> new IllegalArgumentException("No se encontró el tipo de habitación solicitado")); + + if (!Boolean.TRUE.equals(tipoHabitacion.getActiva())) { + throw new IllegalStateException("El tipo de habitación no está disponible actualmente"); + } + if (tipoHabitacion.getAforoMaximo() < numeroHuespedes) { + throw new IllegalArgumentException("El número de huéspedes excede la capacidad del tipo de habitación"); + } + + if (!verificarDisponibilidad(tipoHabitacionIdNormalizado, entrada, salida, numeroHuespedes)) { + throw new IllegalStateException("No hay disponibilidad para las fechas seleccionadas"); + } + + long noches = ChronoUnit.DAYS.between(entrada, salida); + if (noches <= 0) { + throw new IllegalArgumentException("La estancia debe incluir al menos una noche"); + } + + Reserva reserva = new Reserva(); + reserva.setId(UUID.randomUUID().toString()); + reserva.setCodigo(generarCodigoReserva()); + reserva.setCliente(cliente); + reserva.setNotas(notas); + + Estancia estancia = new Estancia(); + estancia.setId(UUID.randomUUID().toString()); + estancia.setCheckIn(Boolean.FALSE); + estancia.setCheckOut(Boolean.FALSE); + estancia.setEntrada(entrada); + estancia.setSalida(salida); + estancia.setNumeroHuespedes(numeroHuespedes); + estancia.setReserva(reserva); + estancia.setTipoHabitacion(tipoHabitacion); + estancia.setPrecioPorNoche(tipoHabitacion.getPrecioPorNoche()); + BigDecimal totalEstadia = tipoHabitacion.getPrecioPorNoche() + .multiply(BigDecimal.valueOf(noches)); + estancia.setTotalEstadia(totalEstadia); + + reserva.setEstancias(List.of(estancia)); + + Reserva nuevaReserva = crear(reserva); + + return nuevaReserva; + } + @Override public double calcularTotalReserva(String id) { Reserva reserva = obtenerYValidar(id); + cargarResumenPago(reserva); ResumenPago resumen = reserva.getResumenPago(); - return resumen.getTotalReserva().doubleValue(); + + if (resumen != null && resumen.getTotalReserva() != null) { + return resumen.getTotalReserva().doubleValue(); + } + + BigDecimal totalHabitaciones = calcularTotalHabitaciones(reserva); + BigDecimal totalServicios = calcularTotalServicios(reserva); + return totalHabitaciones.add(totalServicios).doubleValue(); + } + + private BigDecimal calcularTotalHabitaciones(Reserva reserva) { + if (reserva.getEstancias() == null) { + return BigDecimal.ZERO; + } + return reserva.getEstancias().stream() + .map(Estancia::getTotalEstadia) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private BigDecimal calcularTotalServicios(Reserva reserva) { + if (reserva.getReservasServicios() == null) { + return BigDecimal.ZERO; + } + return reserva.getReservasServicios().stream() + .map(ReservaServicio::getTotalServicio) + .filter(Objects::nonNull) + .reduce(BigDecimal.ZERO, BigDecimal::add); + } + + private void cargarResumenPago(Reserva reserva) { + if (reserva == null || reserva.getId() == null) { + return; + } + resumenPagoRepository.findById(reserva.getId()).ifPresent(resumen -> { + resumen.setReserva(reserva); + reserva.setResumenPago(resumen); + }); + } + + private String generarCodigoReserva() { + String codigo; + do { + codigo = "RES-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } while (repository.existsByCodigo(codigo)); + return codigo; } private Reserva obtenerYValidar(String id) { @@ -204,4 +325,12 @@ private void validarReserva(Reserva reserva) { throw new IllegalArgumentException("La reserva debe tener un código"); } } -} \ No newline at end of file + + private String validarIdentificador(String valor, String descripcionCampo) { + String limpio = valor == null ? null : valor.trim(); + if (limpio == null || limpio.isEmpty()) { + throw new IllegalArgumentException("El identificador " + descripcionCampo + " es requerido"); + } + return limpio; + } +}