From a51a1cc54b58c0016f835d2864ca463e31aeb50f Mon Sep 17 00:00:00 2001 From: devfzn Date: Mon, 18 Sep 2023 22:17:12 -0300 Subject: [PATCH] Spring Boot 3: doc, test y prep. para impl.: Aula 2 --- .../api/controller/ConsultaController.java | 5 +- .../consulta/AgendaDeConsultaService.java | 16 +- .../domain/consulta/ConsultaRepository.java | 9 + .../validaciones/HorarioDeAnticipacion.java | 25 ++ .../validaciones/HorarioDeFuncionamiento.java | 23 ++ .../consulta/validaciones/MedicoActivo.java | 26 ++ .../validaciones/MedicoConConsulta.java | 28 ++ .../consulta/validaciones/PacienteActivo.java | 27 ++ .../validaciones/PacienteSinConsulta.java | 28 ++ .../validaciones/ValidadorDeConsultas.java | 7 + .../api/domain/medico/MedicoRepository.java | 13 +- .../domain/paciente/PacienteRepository.java | 8 + .../api/infra/errores/ManejadorDeErrores.java | 19 ++ 010_spring_boot/spring_boot_3.md | 286 +++++++++++++++++- 14 files changed, 498 insertions(+), 22 deletions(-) create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeAnticipacion.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeFuncionamiento.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoActivo.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoConConsulta.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteActivo.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteSinConsulta.java create mode 100644 010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/ValidadorDeConsultas.java diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java index 14e7caf..5855f51 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/controller/ConsultaController.java @@ -3,7 +3,6 @@ package med.voll.api.controller; import jakarta.validation.Valid; import med.voll.api.domain.consulta.AgendaDeConsultaService; import med.voll.api.domain.consulta.DatosAgendarConsulta; -import med.voll.api.domain.consulta.DatosDetalleConsulta; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -25,8 +24,8 @@ public class ConsultaController { @Transactional // ojo de donde se importa esta anotación public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) { System.out.println(datos); - service.agendar(datos); + var response = service.agendar(datos); - return ResponseEntity.ok(new DatosDetalleConsulta(null, null, null, null)); + return ResponseEntity.ok(response); } } diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java index bb154c6..430e488 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java @@ -1,13 +1,15 @@ package med.voll.api.domain.consulta; +import med.voll.api.domain.consulta.validaciones.ValidadorDeConsultas; import med.voll.api.domain.medico.Medico; import med.voll.api.domain.medico.MedicoRepository; -import med.voll.api.domain.paciente.Paciente; import med.voll.api.domain.paciente.PacienteRepository; import med.voll.api.infra.errores.ValidacionDeIntegridad; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class AgendaDeConsultaService { @@ -18,7 +20,10 @@ public class AgendaDeConsultaService { @Autowired private PacienteRepository pacienteRepository; - public void agendar(DatosAgendarConsulta datos) { + @Autowired + List validadores; + + public DatosDetalleConsulta agendar(DatosAgendarConsulta datos) { if (!pacienteRepository.findById(datos.idPaciente()).isPresent()) { throw new ValidacionDeIntegridad("Id de paciente no encontrado"); @@ -27,10 +32,17 @@ public class AgendaDeConsultaService { throw new ValidacionDeIntegridad("Id de médico no encontrado"); } + validadores.forEach(v-> v.validar(datos)); + var paciente = pacienteRepository.findById(datos.idPaciente()).get(); var medico = seleccionarMedico(datos); + if (medico == null) { + throw new ValidacionDeIntegridad("No hay especialistas disponibles para este horario"); + } var consulta = new Consulta(null, medico, paciente, datos.fecha()); consultaRepository.save(consulta); + + return new DatosDetalleConsulta(consulta); } private Medico seleccionarMedico(DatosAgendarConsulta datos) { diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java index 9f6ef4f..812fd84 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java @@ -3,6 +3,15 @@ package med.voll.api.domain.consulta; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; + @Repository public interface ConsultaRepository extends JpaRepository { + + + Boolean existsByPacienteIdAndFechaBetween(Long idPaciente, + LocalDateTime primerHorario, + LocalDateTime ultimoHorario); + + Boolean existsByMedicoIdAndFecha(Long idMedico, LocalDateTime fecha); } diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeAnticipacion.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeAnticipacion.java new file mode 100644 index 0000000..68e91f7 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeAnticipacion.java @@ -0,0 +1,25 @@ +package med.voll.api.domain.consulta.validaciones; + +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import med.voll.api.infra.errores.ValidacionDeIntegridad; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; + +@Component +public class HorarioDeAnticipacion implements ValidadorDeConsultas { + + public void validar(DatosAgendarConsulta datos) { + var ahora = LocalDateTime.now(); + var horaDeConsulta = datos.fecha(); + var diferenciaDe30Min = Duration.between(ahora, horaDeConsulta).toMinutes() < 30; + + if (diferenciaDe30Min) { + throw new ValidacionDeIntegridad("La consulta debe ser agendada con al menos" + +" 30 minutos de anticipación"); + } + + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeFuncionamiento.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeFuncionamiento.java new file mode 100644 index 0000000..6ea06e3 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/HorarioDeFuncionamiento.java @@ -0,0 +1,23 @@ +package med.voll.api.domain.consulta.validaciones; + +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import med.voll.api.infra.errores.ValidacionDeIntegridad; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.DayOfWeek; + +@Component +public class HorarioDeFuncionamiento implements ValidadorDeConsultas { + + public void validar(DatosAgendarConsulta datos) { + var domingo = DayOfWeek.SUNDAY.equals(datos.fecha().getDayOfWeek()); + var antesDeApertura = datos.fecha().getHour() < 7; + var despuesDeCierre = datos.fecha().getHour() > 19; + + if (domingo || antesDeApertura || despuesDeCierre) { + throw new ValidacionDeIntegridad("El horario de atención es del Lunes a Sábado," + +" de 07:00 a 19:00 horas"); + } + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoActivo.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoActivo.java new file mode 100644 index 0000000..0b24691 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoActivo.java @@ -0,0 +1,26 @@ +package med.voll.api.domain.consulta.validaciones; + +import jakarta.validation.ValidationException; +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import med.voll.api.domain.medico.MedicoRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MedicoActivo implements ValidadorDeConsultas { + + @Autowired + private MedicoRepository repository; + + public void validar(DatosAgendarConsulta datos) { + if (datos.idMedico() == null) { + return; + } + + var medicoActivo = repository.findActivoById(datos.idMedico()); + if (!medicoActivo) { + throw new ValidationException("No se permite agendar una consulta," + +" medico no se encuentra activo"); + } + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoConConsulta.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoConConsulta.java new file mode 100644 index 0000000..c5b946c --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/MedicoConConsulta.java @@ -0,0 +1,28 @@ +package med.voll.api.domain.consulta.validaciones; + +import jakarta.validation.ValidationException; +import med.voll.api.domain.consulta.ConsultaRepository; +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MedicoConConsulta implements ValidadorDeConsultas { + + @Autowired + private ConsultaRepository repositorio; + + public void validar(DatosAgendarConsulta datos) { + + if (datos.idMedico() == null) { + return; + } + + var medicoConConsulta = repositorio.existsByMedicoIdAndFecha(datos.idMedico(), datos.fecha()); + + if (medicoConConsulta) { + throw new ValidationException("Este médico ya tiene agendada una consulta en este horario"); + } + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteActivo.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteActivo.java new file mode 100644 index 0000000..5be37d7 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteActivo.java @@ -0,0 +1,27 @@ +package med.voll.api.domain.consulta.validaciones; + +import jakarta.validation.ValidationException; +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import med.voll.api.domain.paciente.PacienteRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class PacienteActivo implements ValidadorDeConsultas { + + @Autowired + private PacienteRepository repository; + + public void validar(DatosAgendarConsulta datos) { + if (datos.idPaciente() == null) { + return; + } + + var pacienteActivo = repository.findActivoById(datos.idPaciente()); + + if (!pacienteActivo) { + throw new ValidationException("No se permite agendar una consulta," + +" paciente no se encuentra activo"); + } + } +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteSinConsulta.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteSinConsulta.java new file mode 100644 index 0000000..780e826 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/PacienteSinConsulta.java @@ -0,0 +1,28 @@ +package med.voll.api.domain.consulta.validaciones; + +import jakarta.validation.ValidationException; +import med.voll.api.domain.consulta.ConsultaRepository; +import med.voll.api.domain.consulta.DatosAgendarConsulta; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class PacienteSinConsulta implements ValidadorDeConsultas { + + @Autowired + private ConsultaRepository repositorio; + + public void validar(DatosAgendarConsulta datos) { + var primerHorario = datos.fecha().withHour(7); + var ultimoHorario = datos.fecha().withHour(18); + + var pacienteConConsulta = repositorio.existsByPacienteIdAndFechaBetween( + datos.idPaciente(), + primerHorario, + ultimoHorario); + if (pacienteConConsulta) { + throw new ValidationException("No se permite agendar mas de una consulta por día"); + } + } + +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/ValidadorDeConsultas.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/ValidadorDeConsultas.java new file mode 100644 index 0000000..919e5c7 --- /dev/null +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/consulta/validaciones/ValidadorDeConsultas.java @@ -0,0 +1,7 @@ +package med.voll.api.domain.consulta.validaciones; + +import med.voll.api.domain.consulta.DatosAgendarConsulta; + +public interface ValidadorDeConsultas { + public void validar(DatosAgendarConsulta datos); +} diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java index b9efddf..978a5b8 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/medico/MedicoRepository.java @@ -44,10 +44,11 @@ public interface MedicoRepository extends JpaRepository { """) Medico seleccionarMedicoConEspecialidadEnFecha(Especialidad especialidad, LocalDateTime fecha); - //@Query(""" - // select m.activo - // from Medico m - // where m.id=:idMedico - // """) - //Boolean findActivoById(Long idMedico); + @Query(""" + SELECT m.activo + FROM Medico m + WHERE m.id=:idMedico + """) + Boolean findActivoById(Long idMedico); + } diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java index ab69c37..2795f16 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/domain/paciente/PacienteRepository.java @@ -3,7 +3,15 @@ package med.voll.api.domain.paciente; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface PacienteRepository extends JpaRepository { Page findByActivoTrue(Pageable paginacion); + + @Query(""" + SELECT p.activo + FROM Paciente p + WHERE p.id=:idPaciente + """) + Boolean findActivoById(Long idPaciente); } diff --git a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java index fe6db0c..cbaba09 100644 --- a/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java +++ b/010_spring_boot/api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java @@ -1,27 +1,42 @@ package med.voll.api.infra.errores; import jakarta.persistence.EntityNotFoundException; +import jakarta.validation.ValidationException; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class ManejadorDeErrores { @ExceptionHandler(EntityNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) public ResponseEntity manejarError404(){ return ResponseEntity.notFound().build(); } @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) public ResponseEntity manejarError400(MethodArgumentNotValidException e){ var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList(); return ResponseEntity.badRequest().body(errores); } + @ExceptionHandler(ValidacionDeIntegridad.class) + public ResponseEntity errorHandlerValidacionesDeIntegridad(Exception e){ + return ResponseEntity.badRequest().body(e.getMessage()); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity errorHandlerValidacionesDeNegocio(Exception e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + @ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity manejarError500(DataIntegrityViolationException e) { var errores = e.getMostSpecificCause().getLocalizedMessage(); @@ -33,4 +48,8 @@ public class ManejadorDeErrores { this(error.getField(), error.getDefaultMessage()); } } + + //@ExceptionHandler(DataIntegrityViolationException.class) + //@ExceptionHandler(MethodArgumentNotValidException.class) + } diff --git a/010_spring_boot/spring_boot_3.md b/010_spring_boot/spring_boot_3.md index d9d7944..1e1085c 100644 --- a/010_spring_boot/spring_boot_3.md +++ b/010_spring_boot/spring_boot_3.md @@ -506,6 +506,7 @@ consulta utilizando la sintaxis del ***Java Persistence Query Language (JPQL)*** ``` ### En resumen + Creación de nuevo *package* [`domain.consulta`](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/) donde se crean entidad **Consulta**, clases `ConsultaRepository`, @@ -514,8 +515,8 @@ donde se crean entidad **Consulta**, clases `ConsultaRepository`, - [Consulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/Consulta.java) - [ConsultaRepository](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/ConsultaRepository.java) - [DatosAgendarConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosAgendarConsulta.java) -- [DatosDetalleConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java) -- [AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java) + - [DatosDetalleConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java) + - [AgendaDeConsultasService](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/AgendaDeConsultaService.java) - [migración](./api_rest/api3/src/main/resources/db/migration/V6__create-table-consultas.sql) --- @@ -527,9 +528,9 @@ con los atributos de las clases DTO, se pueden utilizar alias ```java public record DatosCompra( - @JsonAlias("producto_id") Long idProducto, - @JsonAlias("fecha_compra") LocalDate fechaCompra - ){} + @JsonAlias("producto_id") Long idProducto, + @JsonAlias("fecha_compra") LocalDate fechaCompra + ){} ``` La anotación `@JsonAlias` sirve para mapear *alias* alternativos para los campos @@ -537,9 +538,9 @@ que se recibirán del JSON, y es posible asignar múltiples alias: ```java public record DatosCompra( - @JsonAlias({"producto_id", "id_producto"}) Long idProducto, - @JsonAlias({"fecha_compra", "fecha"}) LocalDate fechaCompra - ){} + @JsonAlias({"producto_id", "id_producto"}) Long idProducto, + @JsonAlias({"fecha_compra", "fecha"}) LocalDate fechaCompra + ){} ``` ### Formato fechas @@ -605,10 +606,273 @@ Entonces, se podrían crear los siguientes **Services**: Pero es importante estar atentos, ya que muchas veces no es necesario crear un **Service** y, por lo tanto, agregar otra capa y complejidad innecesarias a -una aplicación. Una regla que podemos utilizar es la siguiente: si no hay reglas -de negocio, simplemente se puede realizar la comunicación directa entre los -controllers y los repositories de la aplicación. +una aplicación. Una regla que se puede utilizar es la siguiente: + +***si no hay reglas de negocio, simplemente se puede realizar la comunicación +directa entre los controllers y los repositories de la aplicación.*** --- +## Principios SOLID + +**SOLID** es un acrónimo que representa cinco principios de programación + +- Principio de **Responsabilidad Única** (*Single Responsibility Principle*) +- Principio **Abierto-Cerrado** (*Open-Closed Principle*) +- Principio de **Sustitución de Liskov** (*Liskov Substitution Principle*) +- Principio de **Segregación de Interfaces** (*Interface Segregation Principle*) +- Principio de **Inversión de Dependencia** (*Dependency Inversion Principle*) + +Cada principio representa una buena práctica de programación que, cuando se +aplica en una aplicación, facilita mucho su mantenimiento y extensión. Estos +principios fueron creados por *Robert Martin*, conocido como *Uncle Bob*, en su +artículo ***Design Principles and Design Patterns***. + + +--- + +## Reglas de negocio + +Para cada validación de las reglas de negocio, se crea una clase específica. +La primera regla trata sobre el horario de la clínica: ***"El horario de +funcionamiento de la clínica es de lunes a sábado, de 07:00 a 19:00"***. +Si llega una solicitud a nuestra API con una fecha programada para una consulta, +¿qué sucede si el cliente intenta programar una consulta para un domingo? +¿O para un lunes a las 4 de la mañana? Por esto es necesario validar el horario +de la consulta. + +Creción de subpaquete `consulta.validaciones`. + +Dentro de `validaciones`, se crean las clases. Primero, una nueva clase llamada +`HorarioDeFuncionamientoClinica`. La idea es crear un método dentro de esta +clase para realizar la validación del horario de funcionamiento de la clínica. +Crearemos el método `validar()` y recibiremos el DTO `DatosAgendarConsulta` como +parámetro. + +```java + public class HorarioDeFuncionamientoClinica{ + public void validar(DatosAgendarConsulta datos) { + var domingo = DayOfWeek.SUNDAY.equals(datos.fecha().getDayOfWeek()); + var antesdDeApertura=datos.fecha().getHour()<7; + var despuesDeCierre=datos.fecha().getHour()>19; + if(domingo || antesdDeApertura || despuesDeCierre){ + throw new ValidationException( + "El horario de atención de la clínica es de lunes a + +"sábado, de 07:00 a 19:00 horas"); + } + } + } +``` + +Esto concluye la primera validación. El único objetivo de esta clase es +ejecutar esa única validación. El código queda pequeño, simple y fácil de dar +mantenimiento y de probar de manera automatizada. + +La siguiente validación de la lista es ***"Las consultas tienen una duración +fija de 1 hora"***. Esta validación será implícita, la aplicación estará +disponible de una en una hora para agendar la consulta. + +Siguiente validación: ***"Las consultas deben ser agendadas con un mínimo de 30 +minutos de antelación"***. + +Dentro del paquete de validaciones, se crea una nueva clase llamada +`HorarioDeAnticipacion`.Con un método similar al creado anteriormente. + +```java +public void validar(DatosAgendarConsulta datos) { + var ahora = LocalDateTime.now(); + var horaDeConsulta= datos.fecha(); + var diferenciaDe30Min= Duration.between(ahora,horaDeConsulta).toMinutes()<30; + if(diferenciaDe30Min){ + throw new ValidationException( + "Las consultas deben programarse con al " + +"menos 30 minutos de anticipación"); + } +} +``` + +En la clase `MedicoActivo`, la única diferencia es el uso del Repositorio. +Se está buscando solo el atributo activo del médico, filtrando por el Ii. +Y si el médico no tiene ID, lanza una excepción. + +Se creó el método `findAtivoByID`. En este caso, no se quiere cargar el objeto +completo del médico solo para verificar si el atributo activo es `true`. +Entonces, se puede hacer una consulta personalizada trayendo solo un único +atributo, `SELECT m.activo`. + +Luego en la clase `MedicoConConsulta` también es necesario consultar la base de +datos para verificar si hay una consulta con este médico en la misma fecha. + +Se realiza la consulta usando el patrón de nomenclatura de SpringData: +`existsByMedicoIdAndFecha(idMedico, fecha)`. + +`PacienteActivo` se parece a `ValidadorMedicoActivo`. +Con la validación para que el paciente no tenga consulta en la misma fecha, +`PacienteSinConsulta`. También consulta la base de datos para ver si hay una +consulta para este paciente, colocando la fecha de inicio y la fecha de +finalización de ese día, tomando la primera y la última hora del día. + +Se crea una interfáz `ValidadorDeConsultas` y dentro de esta se declara el +método que las clases tienen en común, `validar(DatosAgendarConsulta datos)`. +No es necesario usar la palabra clave `public` ya que ***es implícito que todos +los métodos de una interfaz son públicos***. + +```java + public interface ValidadorDeConsultas { + public void validar(DatosAgendarConsulta datos); + } +``` + +De esta manera, se estandariza el proyecto. Cada validador debe implementa esta +interfaz y obligatoriamente, deben implementar el método +`validar(DatosAgendarConsulta)`. Solo el cuerpo del método de cada clase será +diferente. + +Otra cosa importante: para poder inyectar estas clases en algún lugar, Spring +necesita conocerlas. Entonces, encima de ellas debe haber alguna anotación de +Spring. + +Se podría usar la anotación `@Service` para indicar que es un servicio, una +validación. Pero se usa la anotación `@Component` que es para componentes +genéricos. Porque en ocasiones se tiene una clase que no es ni una clase de +configuración, ni un controlador ni un servicio. Entonces el `@Component` +indica a Spring que esta clase es un componente genérico y lo cargará en la +inicialización del proyecto. Podría ser `@Service` también. No olvidar las +respectivas anotaciones `@Autowired` + +En la interfaz no es necesario colocar ninguna anotación porque el Spring la +carga automáticamente. + +Para inyectar estos validadores en la clase service, la clase +`AgendaDeConsultaService`, se usa un esquema de Spring que facilita la vida. + +Se puede pensar que se necesita inyectar cada uno de los validadores de la misma +manera que se están inyectando los repositorios. Pero ese es el problema que +se quiere evitar, no se desea declararlos uno por uno. Así que se hace un "truco" +que Spring permite hacer. + +En el código de AgendaDeConsultaService, se declara un atributo, con la anotación +`@Autowired`, pero este atributo es declarado como una **lista** `java.util.List` +y, dentro de la lista, se declara la **interfaz** `ValidadorDeConsultas` y es +llamada `validadores`. + +```java +@Autowired +List validadores; +``` + +Spring identifica automáticamente que se esta inyectando una lista y buscará +todas las clases que implementan la interfaz `ValidadorDeConsultas`. Así, no +importa la cantidad de validadores, Spring inyectará uno por uno. Es mucho más +práctico hacerlo de esta manera. + +En el método agendar(), antes de crear las variables del paciente y del médico, +se obtener esta lista de validadores y se recorre con +`forEach(v -> v.validar(datos))`. + +```java +validadores.forEach(v-> v.validar(datos)); +``` + +De esta forma se logran inyectar todos los validadores y el código queda bastante +flexible. + +Si se quiere excluir un validador, basta con eliminar la clase de ese validador. +No es necesario modificar la clase `AgendaDeConsultaService`, la lista simplemente +quedará con una clase menos. + +Si cambia una validación o si deja de existir, no es necesario modificar la clase +de servicio. + +Probar agendar una cita, **POST** `http://localhost:8080/consultas` + +```json +{ + "idPaciente": 1, + "idMedico": 1, + "fecha": "2023-09-18T10:00, +} +``` + +```java +public DatosDetalleConsulta agendar(DatosAgendarConsulta datos){ + if(!pacienteRepository.findById(datos.idPaciente()).isPresent()){ + throw new ValidacionDeIntegridad("este id para el paciente no fue encontrado"); + } + if(datos.idMedico()!=null && !medicoRepository.existsById(datos.idMedico())){ + throw new ValidacionDeIntegridad("este id para el medico no fue encontrado"); + } + validadores.forEach(v-> v.validar(datos)); + var paciente = pacienteRepository.findById(datos.idPaciente()).get(); + var medico = seleccionarMedico(datos); + if(medico==null){ + throw new ValidacionDeIntegridad("No hay especialistas disponibles " + +" para este horario"); + } + var consulta = new Consulta(medico,paciente,datos.fecha()); + consultaRepository.save(consulta); + return new DatosDetalleConsulta(consulta); +} +``` + +El constructor que recibe el objeto consulta en +[DatosDetalleConsulta](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/DatosDetalleConsulta.java) + +```java +public record DatosDetalleConsulta(Long id, + Long idPaciente, + Long idMedico, + LocalDateTime fecha) { + public DatosDetalleConsulta(Consulta consulta) { + this(consulta.getId(), + consulta.getPaciente().getId(), + consulta.getMedico().getId(), + consulta.getFecha()); + } +} +``` + +Se tiene un DTO con los datos siendo devueltos correctamente. + +En `ConsultaController`, el método `agendar()` devuelve el DTO. Se guarda el DTO +en una variable y en `ResponseEntity.ok` se pasa el DTO que fue devuelto por el +service. + +```java +@PostMapping +@Transactional +public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) { + var dto= service.agendar(datos); + return ResponseEntity.ok(dto); +} +``` + +El envio de un paciente que no existe en la base de datos: + +**POST** `http://localhost:8080/consultas` + +```json +{ + "idPaciente": 88888, + "idMedico": 1, + "fecha": "2023-10-10T10:00" +} +``` + +En el paquete [errores](./api_rest/api3/src/main/java/med/voll/api/infra/errores), +en clase +[ManejadorDeErrores](./api_rest/api3/src/main/java/med/voll/api/infra/errores/ManejadorDeErrores.java) +se crea un nuevo método para `ValidationException`: + +```java +@ExceptionHandler(ValidationException.class) +public ResponseEntity errorHandlerValidacionesDeNegocio(Exception e){ + return ResponseEntity.badRequest().body(e.getMessage()); +} +``` + +--- + +## Documentación +