summaryrefslogtreecommitdiff
path: root/api/src/main/java/med
diff options
context:
space:
mode:
authorlucashemi <lucasxberger@gmail.com>2023-02-24 16:14:01 -0300
committerlucashemi <lucasxberger@gmail.com>2023-02-24 16:14:01 -0300
commit9f82c403098bb96acd8b7116f84416d3b4643b57 (patch)
treef69200376357e803dab229ff5cfea0c001e1803c /api/src/main/java/med
parent514f2e7194a875cfc53d7e1bccd922db2bbb3f3f (diff)
api-appointments
Diffstat (limited to 'api/src/main/java/med')
-rw-r--r--api/src/main/java/med/voll/api/controller/AppointmentController.java34
-rw-r--r--api/src/main/java/med/voll/api/controller/AuthenticationController.java3
-rw-r--r--api/src/main/java/med/voll/api/controller/DoctorController.java2
-rw-r--r--api/src/main/java/med/voll/api/controller/PatientController.java2
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/Appointment.java47
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentDeletionData.java11
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentListingData.java10
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentRegistrationData.java17
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentRepository.java18
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java19
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java75
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/ReasonForCancellation.java8
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/AppointmentCancellationValidator.java7
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/ValidatesTimeInAdvance.java27
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/AppointmentSchedulingValidator.java7
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActiveDoctor.java26
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActivePatient.java21
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDateAndTime.java21
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDoctorWithOtherAppointmentAtTheSameTime.java21
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesPatientDoesntHaveAnotherAppointmentToday.java24
-rw-r--r--api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesTimeInAdvance.java21
-rw-r--r--api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java20
-rw-r--r--api/src/main/java/med/voll/api/domain/patient/PatientRepository.java7
-rw-r--r--api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java5
-rw-r--r--api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java1
-rw-r--r--api/src/main/java/med/voll/api/infra/security/TokenService.java2
-rw-r--r--api/src/main/java/med/voll/api/infra/springdoc/SpringDocConfigurations.java34
27 files changed, 488 insertions, 2 deletions
diff --git a/api/src/main/java/med/voll/api/controller/AppointmentController.java b/api/src/main/java/med/voll/api/controller/AppointmentController.java
new file mode 100644
index 0000000..e5a2ba4
--- /dev/null
+++ b/api/src/main/java/med/voll/api/controller/AppointmentController.java
@@ -0,0 +1,34 @@
+package med.voll.api.controller;
+
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import jakarta.validation.Valid;
+import med.voll.api.domain.appointment.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.ResponseEntity;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.util.UriComponentsBuilder;
+
+@RestController
+@RequestMapping("/appointments")
+@SecurityRequirement(name = "bearer-key")
+public class AppointmentController {
+ @Autowired
+ private AppointmentsSchedule appointmentsSchedule;
+
+ @PostMapping
+ @Transactional
+ public ResponseEntity register(@RequestBody @Valid AppointmentRegistrationData data, UriComponentsBuilder uriBuilder) {
+ var dto = appointmentsSchedule.schedule(data);
+ return ResponseEntity.ok(dto);
+ }
+
+ @DeleteMapping
+ @Transactional
+ public ResponseEntity cancel(@RequestBody @Valid AppointmentDeletionData data) {
+ appointmentsSchedule.cancel(data);
+ return ResponseEntity.noContent().build();
+ }
+
+
+}
diff --git a/api/src/main/java/med/voll/api/controller/AuthenticationController.java b/api/src/main/java/med/voll/api/controller/AuthenticationController.java
index b7dcf27..f86fc02 100644
--- a/api/src/main/java/med/voll/api/controller/AuthenticationController.java
+++ b/api/src/main/java/med/voll/api/controller/AuthenticationController.java
@@ -14,8 +14,9 @@ import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
-@Controller
+@RestController
@RequestMapping("/login")
public class AuthenticationController {
diff --git a/api/src/main/java/med/voll/api/controller/DoctorController.java b/api/src/main/java/med/voll/api/controller/DoctorController.java
index a262d34..f87da56 100644
--- a/api/src/main/java/med/voll/api/controller/DoctorController.java
+++ b/api/src/main/java/med/voll/api/controller/DoctorController.java
@@ -1,5 +1,6 @@
package med.voll.api.controller;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import med.voll.api.domain.doctor.DoctorListingData;
import med.voll.api.domain.doctor.Doctor;
@@ -17,6 +18,7 @@ import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping("/doctors")
+@SecurityRequirement(name = "bearer-key")
public class DoctorController {
@Autowired
diff --git a/api/src/main/java/med/voll/api/controller/PatientController.java b/api/src/main/java/med/voll/api/controller/PatientController.java
index bd774a1..5675cfb 100644
--- a/api/src/main/java/med/voll/api/controller/PatientController.java
+++ b/api/src/main/java/med/voll/api/controller/PatientController.java
@@ -1,5 +1,6 @@
package med.voll.api.controller;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import med.voll.api.domain.patient.PatientListingData;
import med.voll.api.domain.patient.Patient;
@@ -17,6 +18,7 @@ import org.springframework.web.util.UriComponentsBuilder;
@RestController
@RequestMapping("/patients")
+@SecurityRequirement(name = "bearer-key")
public class PatientController {
@Autowired
PatientRepository patientRepository;
diff --git a/api/src/main/java/med/voll/api/domain/appointment/Appointment.java b/api/src/main/java/med/voll/api/domain/appointment/Appointment.java
new file mode 100644
index 0000000..a48c15f
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/Appointment.java
@@ -0,0 +1,47 @@
+package med.voll.api.domain.appointment;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import med.voll.api.domain.doctor.Doctor;
+import med.voll.api.domain.patient.Patient;
+
+import java.time.LocalDateTime;
+
+@Table(name = "appointments")
+@Entity(name = "Appointment")
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+@EqualsAndHashCode(of = "id")
+public class Appointment {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ private LocalDateTime date;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "doctor_id")
+ private Doctor doctor;
+ @ManyToOne(fetch = FetchType.LAZY)
+ @JoinColumn(name = "patient_id")
+ private Patient patient;
+ private Boolean active = true;
+
+ @Column(name = "reason_for_cancellation")
+ @Enumerated(EnumType.STRING)
+ private ReasonForCancellation reasonForCancellation;
+
+ public Appointment(Long id, LocalDateTime date, Doctor doctor, Patient patient) {
+ this.id = id;
+ this.date = date;
+ this.doctor = doctor;
+ this.patient = patient;
+ }
+
+ public void cancel(ReasonForCancellation reasonForCancellation) {
+ this.reasonForCancellation = reasonForCancellation;
+ this.active = false;
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentDeletionData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentDeletionData.java
new file mode 100644
index 0000000..81dc28e
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentDeletionData.java
@@ -0,0 +1,11 @@
+package med.voll.api.domain.appointment;
+
+import jakarta.validation.constraints.NotNull;
+
+public record AppointmentDeletionData(
+ @NotNull
+ Long idAppointment,
+ @NotNull
+ ReasonForCancellation reasonForCancellation
+) {
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentListingData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentListingData.java
new file mode 100644
index 0000000..4070a4c
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentListingData.java
@@ -0,0 +1,10 @@
+package med.voll.api.domain.appointment;
+
+import java.time.LocalDateTime;
+
+public record AppointmentListingData(Long id, LocalDateTime date, Long idDoctor, Long idPatient) {
+
+ public AppointmentListingData(Appointment appointment) {
+ this(appointment.getId(), appointment.getDate(), appointment.getDoctor().getId(), appointment.getPatient().getId());
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentRegistrationData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentRegistrationData.java
new file mode 100644
index 0000000..fa62bde
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentRegistrationData.java
@@ -0,0 +1,17 @@
+package med.voll.api.domain.appointment;
+
+import jakarta.validation.constraints.Future;
+import jakarta.validation.constraints.NotNull;
+import med.voll.api.domain.doctor.Specialty;
+
+import java.time.LocalDateTime;
+
+public record AppointmentRegistrationData(
+ Long idDoctor,
+ @NotNull(message = "Patient is required")
+ Long idPatient,
+ @NotNull
+ @Future
+ LocalDateTime date,
+ Specialty specialty) {
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentRepository.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentRepository.java
new file mode 100644
index 0000000..a56e3c7
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentRepository.java
@@ -0,0 +1,18 @@
+package med.voll.api.domain.appointment;
+
+import med.voll.api.domain.doctor.Doctor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+
+@Repository
+public interface AppointmentRepository extends JpaRepository<Appointment, Long> {
+ Page<Appointment> findAllByActiveTrue(Pageable pagination);
+
+ boolean existsByDoctorIdAndDateAndActiveIsTrue(Long idDoctor, LocalDateTime date);
+
+ boolean existsByPatientIdAndDateBetween(Long idPatient, LocalDateTime earliestTime, LocalDateTime latestTime);
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java
new file mode 100644
index 0000000..c13712c
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentUpdateData.java
@@ -0,0 +1,19 @@
+package med.voll.api.domain.appointment;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+import java.time.LocalDateTime;
+
+public record AppointmentUpdateData(
+ @NotNull
+ Long id,
+ Integer year,
+ Integer month,
+ Integer day,
+ Integer hour,
+ Integer minute,
+ String doctor,
+ String patient
+) {
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java b/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java
new file mode 100644
index 0000000..fd7d4e8
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/AppointmentsSchedule.java
@@ -0,0 +1,75 @@
+package med.voll.api.domain.appointment;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.validations.cancellation.AppointmentCancellationValidator;
+import med.voll.api.domain.appointment.validations.scheduling.AppointmentSchedulingValidator;
+import med.voll.api.domain.doctor.Doctor;
+import med.voll.api.domain.doctor.DoctorRepository;
+import med.voll.api.domain.patient.PatientRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+public class AppointmentsSchedule {
+ @Autowired
+ private AppointmentRepository appointmentRepository;
+ @Autowired
+ private DoctorRepository doctorRepository;
+ @Autowired
+ private PatientRepository patientRepository;
+ @Autowired
+ private List<AppointmentSchedulingValidator> validators;
+
+ @Autowired
+ private List<AppointmentCancellationValidator> cancellationValidators;
+
+ public AppointmentListingData schedule(AppointmentRegistrationData data) {
+ if (!patientRepository.existsById(data.idPatient())) {
+ throw new ValidationException("Patient id doesn't exist");
+ }
+
+ if (data.idDoctor() != null && !doctorRepository.existsById(data.idDoctor())) {
+ throw new ValidationException("Doctor id doesn't exist");
+ }
+
+ validators.forEach(v -> v.validate(data));
+
+ var doctor = chooseDoctor(data);
+ if (doctor == null) {
+ throw new ValidationException("There are no available doctors on this date");
+ }
+ var patient = patientRepository.getReferenceById(data.idPatient());
+
+ var appointment = new Appointment(null, data.date(), doctor, patient, true, null);
+
+ appointmentRepository.save(appointment);
+
+ return new AppointmentListingData(appointment);
+ }
+
+ public void cancel(AppointmentDeletionData data) {
+ if (!appointmentRepository.existsById(data.idAppointment())) {
+ throw new ValidationException("Invalid appointment id");
+ }
+
+ cancellationValidators.forEach(v -> v.validate(data));
+
+ var appointment = appointmentRepository.getReferenceById(data.idAppointment());
+ appointment.cancel(data.reasonForCancellation());
+ }
+
+ private Doctor chooseDoctor(AppointmentRegistrationData data) {
+ if(data.idDoctor() != null) {
+ return doctorRepository.getReferenceById(data.idDoctor());
+ }
+
+ if (data.specialty() == null) {
+ throw new ValidationException("Specialty is mandatory when a doctor is not chosen");
+ }
+
+ return doctorRepository.chooseRandomDoctorAvailable(data.specialty(), data.date());
+ }
+
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/ReasonForCancellation.java b/api/src/main/java/med/voll/api/domain/appointment/ReasonForCancellation.java
new file mode 100644
index 0000000..db6e98a
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/ReasonForCancellation.java
@@ -0,0 +1,8 @@
+package med.voll.api.domain.appointment;
+
+public enum ReasonForCancellation {
+ PATIENT_GAVE_UP,
+ LONG_WAITING_TIME,
+ DOCTOR_CANCELED,
+ OTHER_REASON
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/AppointmentCancellationValidator.java b/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/AppointmentCancellationValidator.java
new file mode 100644
index 0000000..6c9e0cc
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/AppointmentCancellationValidator.java
@@ -0,0 +1,7 @@
+package med.voll.api.domain.appointment.validations.cancellation;
+
+import med.voll.api.domain.appointment.AppointmentDeletionData;
+
+public interface AppointmentCancellationValidator {
+ void validate(AppointmentDeletionData data);
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/ValidatesTimeInAdvance.java b/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/ValidatesTimeInAdvance.java
new file mode 100644
index 0000000..03688ce
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/cancellation/ValidatesTimeInAdvance.java
@@ -0,0 +1,27 @@
+package med.voll.api.domain.appointment.validations.cancellation;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentDeletionData;
+import med.voll.api.domain.appointment.AppointmentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+@Component("ValidatesTimeInAdvanceCancellation")
+public class ValidatesTimeInAdvance implements AppointmentCancellationValidator {
+ @Autowired
+ private AppointmentRepository appointmentRepository;
+
+ @Override
+ public void validate(AppointmentDeletionData data) {
+ var appointment = appointmentRepository.getReferenceById(data.idAppointment());
+ var now = LocalDateTime.now();
+ var differenceInHours = Duration.between(now, appointment.getDate()).toHours();
+
+ if (differenceInHours < 24) {
+ throw new ValidationException("Appointments can only be cancelled with at least 24 hours in advance!");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/AppointmentSchedulingValidator.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/AppointmentSchedulingValidator.java
new file mode 100644
index 0000000..99f6128
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/AppointmentSchedulingValidator.java
@@ -0,0 +1,7 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+
+public interface AppointmentSchedulingValidator {
+ void validate(AppointmentRegistrationData data);
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActiveDoctor.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActiveDoctor.java
new file mode 100644
index 0000000..59bb761
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActiveDoctor.java
@@ -0,0 +1,26 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import med.voll.api.domain.doctor.DoctorRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ValidatesActiveDoctor implements AppointmentSchedulingValidator {
+
+ @Autowired
+ private DoctorRepository doctorRepository;
+
+ public void validate(AppointmentRegistrationData data) {
+ // random doctor
+ if (data.idDoctor() == null) {
+ return;
+ }
+
+ var doctorIsActive = doctorRepository.findActiveById(data.idDoctor());
+ if (!doctorIsActive) {
+ throw new ValidationException("Appointment can't be scheduled with an inactive doctor");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActivePatient.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActivePatient.java
new file mode 100644
index 0000000..439877f
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesActivePatient.java
@@ -0,0 +1,21 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import med.voll.api.domain.patient.PatientRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ValidatesActivePatient implements AppointmentSchedulingValidator {
+
+ @Autowired
+ private PatientRepository patientRepository;
+
+ public void validate(AppointmentRegistrationData data) {
+ var patientIsActive = patientRepository.findActiveById(data.idPatient());
+ if (!patientIsActive) {
+ throw new ValidationException("Appointment can't be scheduled with an inactive patient");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDateAndTime.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDateAndTime.java
new file mode 100644
index 0000000..beee6c6
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDateAndTime.java
@@ -0,0 +1,21 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import org.springframework.stereotype.Component;
+
+import java.time.DayOfWeek;
+
+@Component
+public class ValidatesDateAndTime implements AppointmentSchedulingValidator {
+ public void validate(AppointmentRegistrationData data) {
+ var appointmentDate = data.date();
+ var sunday = appointmentDate.getDayOfWeek().equals(DayOfWeek.SUNDAY);
+ var beforeTheClinicOpens = appointmentDate.getHour() < 7;
+ var afterTheClinicCloses = appointmentDate.getHour() > 18;
+
+ if (sunday || beforeTheClinicOpens || afterTheClinicCloses) {
+ throw new ValidationException("Appointment outside opening hours or date");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDoctorWithOtherAppointmentAtTheSameTime.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDoctorWithOtherAppointmentAtTheSameTime.java
new file mode 100644
index 0000000..fa94d81
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesDoctorWithOtherAppointmentAtTheSameTime.java
@@ -0,0 +1,21 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import med.voll.api.domain.appointment.AppointmentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ValidatesDoctorWithOtherAppointmentAtTheSameTime implements AppointmentSchedulingValidator {
+
+ @Autowired
+ private AppointmentRepository appointmentRepository;
+
+ public void validate(AppointmentRegistrationData data) {
+ var doctorHasAnotherAppointmentAtTheSameTime = appointmentRepository.existsByDoctorIdAndDateAndActiveIsTrue(data.idDoctor(), data.date());
+ if (doctorHasAnotherAppointmentAtTheSameTime) {
+ throw new ValidationException("This doctor already has an appointment at this time and date");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesPatientDoesntHaveAnotherAppointmentToday.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesPatientDoesntHaveAnotherAppointmentToday.java
new file mode 100644
index 0000000..a412bfb
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesPatientDoesntHaveAnotherAppointmentToday.java
@@ -0,0 +1,24 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import med.voll.api.domain.appointment.AppointmentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ValidatesPatientDoesntHaveAnotherAppointmentToday implements AppointmentSchedulingValidator {
+
+ @Autowired
+ private AppointmentRepository appointmentRepository;
+
+ public void validate(AppointmentRegistrationData data) {
+ var earliestTime = data.date().withHour(7);
+ var latestTime = data.date().withHour(18);
+ var patientHasAnotherAppointmentToday = appointmentRepository.existsByPatientIdAndDateBetween(data.idPatient(), earliestTime, latestTime);
+
+ if (patientHasAnotherAppointmentToday) {
+ throw new ValidationException("Patient already has an scheduled appointment today");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesTimeInAdvance.java b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesTimeInAdvance.java
new file mode 100644
index 0000000..442f4f9
--- /dev/null
+++ b/api/src/main/java/med/voll/api/domain/appointment/validations/scheduling/ValidatesTimeInAdvance.java
@@ -0,0 +1,21 @@
+package med.voll.api.domain.appointment.validations.scheduling;
+
+import jakarta.validation.ValidationException;
+import med.voll.api.domain.appointment.AppointmentRegistrationData;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+
+@Component("ValidatesTimeInAdvanceScheduling")
+public class ValidatesTimeInAdvance implements AppointmentSchedulingValidator {
+ public void validate(AppointmentRegistrationData data) {
+ var appointmentDate = data.date();
+ var now = LocalDateTime.now();
+ var differenceInMinutes = Duration.between(now, appointmentDate).toMinutes();
+
+ if (differenceInMinutes < 30) {
+ throw new ValidationException("Appointment should be scheduled half an hour in advance");
+ }
+ }
+}
diff --git a/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java b/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java
index 1efd0af..8cd6c46 100644
--- a/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java
+++ b/api/src/main/java/med/voll/api/domain/doctor/DoctorRepository.java
@@ -3,9 +3,29 @@ package med.voll.api.domain.doctor;
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;
import org.springframework.stereotype.Repository;
+import java.time.LocalDateTime;
+
@Repository
public interface DoctorRepository extends JpaRepository<Doctor, Long> {
Page<Doctor> findAllByActiveTrue(Pageable pagination);
+
+ //order by rand() limit 1
+ @Query("""
+ SELECT d from Doctor d where d.active = true
+ and d.specialty = :specialty
+ and d.id not in (
+ select a.doctor.id from Appointment a where a.date = :date
+ and a.active = true
+ )
+ """)
+ Doctor chooseRandomDoctorAvailable(Specialty specialty, LocalDateTime date);
+
+ @Query("""
+ select d.active from Doctor d
+ where d.id = :id
+ """)
+ Boolean findActiveById(Long id);
}
diff --git a/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java
index e550ac2..dbdcd87 100644
--- a/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java
+++ b/api/src/main/java/med/voll/api/domain/patient/PatientRepository.java
@@ -3,9 +3,16 @@ package med.voll.api.domain.patient;
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;
import org.springframework.stereotype.Repository;
@Repository
public interface PatientRepository extends JpaRepository<Patient, Long> {
Page<Patient> findAllByActiveTrue(Pageable pagination);
+
+ @Query("""
+ select p.active from Patient p
+ where p.id = :id
+ """)
+ boolean findActiveById(Long id);
}
diff --git a/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java b/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java
index 9e49fd3..f2d40cc 100644
--- a/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java
+++ b/api/src/main/java/med/voll/api/infra/exception/ErrorTreatment.java
@@ -1,6 +1,7 @@
package med.voll.api.infra.exception;
import jakarta.persistence.EntityNotFoundException;
+import jakarta.validation.ValidationException;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -20,6 +21,10 @@ public class ErrorTreatment {
var errors = ex.getFieldErrors();
return ResponseEntity.badRequest().body(errors.stream().map(ValidationErrorData::new).toList());
}
+ @ExceptionHandler(ValidationException.class)
+ public ResponseEntity treatBusinessRule(ValidationException ex) {
+ return ResponseEntity.badRequest().body(ex.getMessage());
+ }
private record ValidationErrorData(String field, String message) {
public ValidationErrorData(FieldError error) {
diff --git a/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java b/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java
index 5055162..46038c6 100644
--- a/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java
+++ b/api/src/main/java/med/voll/api/infra/security/SecurityConfigurations.java
@@ -27,6 +27,7 @@ public class SecurityConfigurations {
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeHttpRequests()
.requestMatchers(HttpMethod.POST, "/login").permitAll()
+ .requestMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**").permitAll()
.anyRequest().authenticated()
.and().addFilterBefore(securityFilter, UsernamePasswordAuthenticationFilter.class)
.build();
diff --git a/api/src/main/java/med/voll/api/infra/security/TokenService.java b/api/src/main/java/med/voll/api/infra/security/TokenService.java
index 63dda73..bf19a3d 100644
--- a/api/src/main/java/med/voll/api/infra/security/TokenService.java
+++ b/api/src/main/java/med/voll/api/infra/security/TokenService.java
@@ -45,6 +45,6 @@ public class TokenService {
}
private Instant expirationDate() {
- return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
+ return LocalDateTime.now().plusHours(12).toInstant(ZoneOffset.of("-03:00"));
}
}
diff --git a/api/src/main/java/med/voll/api/infra/springdoc/SpringDocConfigurations.java b/api/src/main/java/med/voll/api/infra/springdoc/SpringDocConfigurations.java
new file mode 100644
index 0000000..c575160
--- /dev/null
+++ b/api/src/main/java/med/voll/api/infra/springdoc/SpringDocConfigurations.java
@@ -0,0 +1,34 @@
+package med.voll.api.infra.springdoc;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.security.SecurityScheme;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringDocConfigurations {
+
+ @Bean
+ public OpenAPI customOpenAPI() {
+ return new OpenAPI()
+ .components(new Components()
+ .addSecuritySchemes("bearer-key",
+ new SecurityScheme()
+ .type(SecurityScheme.Type.HTTP)
+ .scheme("bearer")
+ .bearerFormat("JWT")))
+ .info(new Info()
+ .title("Voll.med API")
+ .description("Voll.med Rest API application, containing CRUD functionalities for doctors and patients, in addition to scheduling and canceling appointments")
+ .contact(new Contact()
+ .name("Backend Team")
+ .email("backend@voll.med"))
+ .license(new License()
+ .name("Apache 2.0")
+ .url("http://voll.med/api/license")));
+ }
+}