Spring Boot 3: doc, test y prep. para impl.: Aula 2
This commit is contained in:
parent
3b667e35cb
commit
a51a1cc54b
@ -3,7 +3,6 @@ package med.voll.api.controller;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import med.voll.api.domain.consulta.AgendaDeConsultaService;
|
import med.voll.api.domain.consulta.AgendaDeConsultaService;
|
||||||
import med.voll.api.domain.consulta.DatosAgendarConsulta;
|
import med.voll.api.domain.consulta.DatosAgendarConsulta;
|
||||||
import med.voll.api.domain.consulta.DatosDetalleConsulta;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@ -25,8 +24,8 @@ public class ConsultaController {
|
|||||||
@Transactional // ojo de donde se importa esta anotación
|
@Transactional // ojo de donde se importa esta anotación
|
||||||
public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) {
|
public ResponseEntity agendar(@RequestBody @Valid DatosAgendarConsulta datos) {
|
||||||
System.out.println(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package med.voll.api.domain.consulta;
|
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.Medico;
|
||||||
import med.voll.api.domain.medico.MedicoRepository;
|
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.domain.paciente.PacienteRepository;
|
||||||
import med.voll.api.infra.errores.ValidacionDeIntegridad;
|
import med.voll.api.infra.errores.ValidacionDeIntegridad;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AgendaDeConsultaService {
|
public class AgendaDeConsultaService {
|
||||||
|
|
||||||
@ -18,7 +20,10 @@ public class AgendaDeConsultaService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PacienteRepository pacienteRepository;
|
private PacienteRepository pacienteRepository;
|
||||||
|
|
||||||
public void agendar(DatosAgendarConsulta datos) {
|
@Autowired
|
||||||
|
List<ValidadorDeConsultas> validadores;
|
||||||
|
|
||||||
|
public DatosDetalleConsulta agendar(DatosAgendarConsulta datos) {
|
||||||
|
|
||||||
if (!pacienteRepository.findById(datos.idPaciente()).isPresent()) {
|
if (!pacienteRepository.findById(datos.idPaciente()).isPresent()) {
|
||||||
throw new ValidacionDeIntegridad("Id de paciente no encontrado");
|
throw new ValidacionDeIntegridad("Id de paciente no encontrado");
|
||||||
@ -27,10 +32,17 @@ public class AgendaDeConsultaService {
|
|||||||
throw new ValidacionDeIntegridad("Id de médico no encontrado");
|
throw new ValidacionDeIntegridad("Id de médico no encontrado");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validadores.forEach(v-> v.validar(datos));
|
||||||
|
|
||||||
var paciente = pacienteRepository.findById(datos.idPaciente()).get();
|
var paciente = pacienteRepository.findById(datos.idPaciente()).get();
|
||||||
var medico = seleccionarMedico(datos);
|
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());
|
var consulta = new Consulta(null, medico, paciente, datos.fecha());
|
||||||
consultaRepository.save(consulta);
|
consultaRepository.save(consulta);
|
||||||
|
|
||||||
|
return new DatosDetalleConsulta(consulta);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Medico seleccionarMedico(DatosAgendarConsulta datos) {
|
private Medico seleccionarMedico(DatosAgendarConsulta datos) {
|
||||||
|
@ -3,6 +3,15 @@ package med.voll.api.domain.consulta;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface ConsultaRepository extends JpaRepository<Consulta, Long> {
|
public interface ConsultaRepository extends JpaRepository<Consulta, Long> {
|
||||||
|
|
||||||
|
|
||||||
|
Boolean existsByPacienteIdAndFechaBetween(Long idPaciente,
|
||||||
|
LocalDateTime primerHorario,
|
||||||
|
LocalDateTime ultimoHorario);
|
||||||
|
|
||||||
|
Boolean existsByMedicoIdAndFecha(Long idMedico, LocalDateTime fecha);
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -44,10 +44,11 @@ public interface MedicoRepository extends JpaRepository<Medico, Long> {
|
|||||||
""")
|
""")
|
||||||
Medico seleccionarMedicoConEspecialidadEnFecha(Especialidad especialidad, LocalDateTime fecha);
|
Medico seleccionarMedicoConEspecialidadEnFecha(Especialidad especialidad, LocalDateTime fecha);
|
||||||
|
|
||||||
//@Query("""
|
@Query("""
|
||||||
// select m.activo
|
SELECT m.activo
|
||||||
// from Medico m
|
FROM Medico m
|
||||||
// where m.id=:idMedico
|
WHERE m.id=:idMedico
|
||||||
// """)
|
""")
|
||||||
//Boolean findActivoById(Long idMedico);
|
Boolean findActivoById(Long idMedico);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,15 @@ package med.voll.api.domain.paciente;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
public interface PacienteRepository extends JpaRepository<Paciente, Long> {
|
public interface PacienteRepository extends JpaRepository<Paciente, Long> {
|
||||||
Page<Paciente> findByActivoTrue(Pageable paginacion);
|
Page<Paciente> findByActivoTrue(Pageable paginacion);
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT p.activo
|
||||||
|
FROM Paciente p
|
||||||
|
WHERE p.id=:idPaciente
|
||||||
|
""")
|
||||||
|
Boolean findActivoById(Long idPaciente);
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,42 @@
|
|||||||
package med.voll.api.infra.errores;
|
package med.voll.api.infra.errores;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
import org.springframework.dao.DataIntegrityViolationException;
|
import org.springframework.dao.DataIntegrityViolationException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
public class ManejadorDeErrores {
|
public class ManejadorDeErrores {
|
||||||
|
|
||||||
@ExceptionHandler(EntityNotFoundException.class)
|
@ExceptionHandler(EntityNotFoundException.class)
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
public ResponseEntity manejarError404(){
|
public ResponseEntity manejarError404(){
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
|
public ResponseEntity manejarError400(MethodArgumentNotValidException e){
|
||||||
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
|
var errores = e.getFieldErrors().stream().map(DatosErrorValidacion::new).toList();
|
||||||
return ResponseEntity.badRequest().body(errores);
|
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)
|
@ExceptionHandler(DataIntegrityViolationException.class)
|
||||||
public ResponseEntity manejarError500(DataIntegrityViolationException e) {
|
public ResponseEntity manejarError500(DataIntegrityViolationException e) {
|
||||||
var errores = e.getMostSpecificCause().getLocalizedMessage();
|
var errores = e.getMostSpecificCause().getLocalizedMessage();
|
||||||
@ -33,4 +48,8 @@ public class ManejadorDeErrores {
|
|||||||
this(error.getField(), error.getDefaultMessage());
|
this(error.getField(), error.getDefaultMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//@ExceptionHandler(DataIntegrityViolationException.class)
|
||||||
|
//@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -506,6 +506,7 @@ consulta utilizando la sintaxis del ***Java Persistence Query Language (JPQL)***
|
|||||||
```
|
```
|
||||||
|
|
||||||
### En resumen
|
### En resumen
|
||||||
|
|
||||||
Creación de nuevo *package*
|
Creación de nuevo *package*
|
||||||
[`domain.consulta`](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/)
|
[`domain.consulta`](./api_rest/api3/src/main/java/med/voll/api/domain/consulta/)
|
||||||
donde se crean entidad **Consulta**, clases `ConsultaRepository`,
|
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)
|
- [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)
|
- [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)
|
- [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)
|
- [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)
|
- [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)
|
- [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
|
```java
|
||||||
public record DatosCompra(
|
public record DatosCompra(
|
||||||
@JsonAlias("producto_id") Long idProducto,
|
@JsonAlias("producto_id") Long idProducto,
|
||||||
@JsonAlias("fecha_compra") LocalDate fechaCompra
|
@JsonAlias("fecha_compra") LocalDate fechaCompra
|
||||||
){}
|
){}
|
||||||
```
|
```
|
||||||
|
|
||||||
La anotación `@JsonAlias` sirve para mapear *alias* alternativos para los campos
|
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
|
```java
|
||||||
public record DatosCompra(
|
public record DatosCompra(
|
||||||
@JsonAlias({"producto_id", "id_producto"}) Long idProducto,
|
@JsonAlias({"producto_id", "id_producto"}) Long idProducto,
|
||||||
@JsonAlias({"fecha_compra", "fecha"}) LocalDate fechaCompra
|
@JsonAlias({"fecha_compra", "fecha"}) LocalDate fechaCompra
|
||||||
){}
|
){}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Formato fechas
|
### 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
|
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
|
**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
|
una aplicación. Una regla que se puede utilizar es la siguiente:
|
||||||
de negocio, simplemente se puede realizar la comunicación directa entre los
|
|
||||||
controllers y los repositories de la aplicación.
|
***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<ValidadorDeConsultas> 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
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user