diff --git a/src/main/java/de/etecture/ga/api/GarageApiController.java b/src/main/java/de/etecture/ga/api/GarageApiController.java index 42cbbea..f4b16d2 100644 --- a/src/main/java/de/etecture/ga/api/GarageApiController.java +++ b/src/main/java/de/etecture/ga/api/GarageApiController.java @@ -11,6 +11,7 @@ import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; @@ -56,8 +57,8 @@ public class GarageApiController implements WerkstattApi { long garageId = Long.parseLong(werkstattId); Optional serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId)) : Optional.empty(); - Optional appointmentsFrom = Optional.ofNullable(parseLocalDateTime(von)); - Optional appointmentsTill = Optional.ofNullable(parseLocalDateTime(bis)); + Optional appointmentsFrom = Optional.ofNullable(parseDate(von)); + Optional appointmentsTill = Optional.ofNullable(parseDate(bis)); log.info("Filter appointments by garage {}, serviceId {}, from {}, till {}", garageId, serviceId, appointmentsFrom, appointmentsTill); @@ -76,15 +77,31 @@ public class GarageApiController implements WerkstattApi { @Override public ResponseEntity postTermin(String werkstattId, @Valid TerminRequest termin) { - // TODO Auto-generated method stub - return WerkstattApi.super.postTermin(werkstattId, termin); + + // input validation + Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); + Assert.isTrue(NumberUtils.isParsable(termin.getLeistungsId()), "leistungsId ungültig"); + Assert.notNull(parseDate(termin.getVon()), "von ungültig"); + Assert.notNull(parseDate(termin.getBis()), "bis ungültig"); + + long garageId = Long.parseLong(werkstattId); + long serviceId = Long.parseLong(termin.getLeistungsId()); + Date appointmentFrom = parseDate(termin.getVon()); + Date appointmentTill = parseDate(termin.getBis()); + + // create appointment if possible + Optional appointment = appointmentService.createAppointment(garageId, serviceId, + appointmentFrom, appointmentTill); + + return appointment.map(a -> ResponseEntity.ok(AppointmentTerminMapper.toTermin(a))) + .orElse(ResponseEntity.status(HttpStatus.CONFLICT).build()); } - private Date parseLocalDateTime(String dateTimeString) { + private Date parseDate(String dateTimeString) { if (StringUtils.isNotBlank(dateTimeString)) { try { - SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy hh:mm"); + SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm"); return format.parse(dateTimeString); } catch (ParseException e) { diff --git a/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java index 96e4f9c..f8048eb 100644 --- a/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java +++ b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java @@ -1,8 +1,5 @@ package de.etecture.ga.dto.mapper; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; import java.time.format.DateTimeFormatter; import de.etecture.ga.dto.Termin; @@ -16,17 +13,13 @@ public class AppointmentTerminMapper { log.info("Mapping Object {}", appointment); - LocalDateTime appointmentStart = Instant.ofEpochMilli(appointment.appointmentTime().getTime()) - .atZone(ZoneId.systemDefault()).toLocalDateTime(); - LocalDateTime appointmentEnd = appointmentStart.plus(appointment.duration()); - return new Termin() .id(Long.toString(appointment.id())) .leistungsId(Long.toString(appointment.serviceId().getId())) .leistung(appointment.serviceName()) .werkstattName("to_be_set") - .von(appointmentStart.format(DateTimeFormatter.ofPattern("dd.MM.yyyy hh:mm"))) - .bis(appointmentEnd.format(DateTimeFormatter.ofPattern("dd.MM.yyyy hh:mm"))); + .von(appointment.appointmentStart().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))) + .bis(appointment.appointmentEnd().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); } } diff --git a/src/main/java/de/etecture/ga/model/Appointment.java b/src/main/java/de/etecture/ga/model/Appointment.java index acece4b..1362d4d 100644 --- a/src/main/java/de/etecture/ga/model/Appointment.java +++ b/src/main/java/de/etecture/ga/model/Appointment.java @@ -1,6 +1,9 @@ package de.etecture.ga.model; import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; import java.util.Date; import org.springframework.data.annotation.Id; @@ -32,4 +35,14 @@ public class Appointment { private Integer slot = 1; private Duration duration = Duration.ZERO; + + + public LocalDateTime appointmentStart() { + return Instant.ofEpochMilli(this.appointmentTime().getTime()) + .atZone(ZoneId.systemDefault()).toLocalDateTime(); + } + + public LocalDateTime appointmentEnd() { + return this.appointmentStart().plus(this.duration()); + } } diff --git a/src/main/java/de/etecture/ga/service/AppointmentService.java b/src/main/java/de/etecture/ga/service/AppointmentService.java index 8e52d04..81791d7 100644 --- a/src/main/java/de/etecture/ga/service/AppointmentService.java +++ b/src/main/java/de/etecture/ga/service/AppointmentService.java @@ -1,13 +1,21 @@ package de.etecture.ga.service; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.stereotype.Service; import de.etecture.ga.model.Appointment; +import de.etecture.ga.model.Garage; +import de.etecture.ga.model.GarageServices; +import de.etecture.ga.model.MDService; import de.etecture.ga.repository.AppointmentRepository; import lombok.AllArgsConstructor; @@ -17,6 +25,10 @@ public class AppointmentService { private final AppointmentRepository repository; + private final GarageService garageService; + + private final MDServiceService serviceService; + public Optional getAppointment(long appointmentId) { return repository.findById(appointmentId); @@ -47,4 +59,63 @@ public class AppointmentService { return appointments.toList(); } + + public Optional createAppointment(long garageId, long serviceId, Date from, Date till) { + + Optional garage = garageService.getGarage(garageId); + if (garage.isEmpty()) + throw new IllegalArgumentException("GarageId not valid"); + + Optional service = serviceService.getMDService(serviceId) + .filter(s -> garage.get().garageServices().stream().anyMatch(gs -> s.id() == gs.serviceId().getId())); + Optional garageService = garage.get().garageServices().stream() + .filter(gs -> serviceId == gs.serviceId().getId()).findFirst(); + if (service.isEmpty() || garageService.isEmpty()) + throw new IllegalArgumentException("serviceId not valid"); + + LocalDateTime fromDateTime = Instant.ofEpochMilli(from.getTime()).atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + LocalDateTime tillDateTime = Instant.ofEpochMilli(till.getTime()).atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + + LocalDateTime validAppointmentTime = null; + while (!fromDateTime.isAfter(tillDateTime)) { + if (isSlotAvailable(garage.get(), fromDateTime)) { + validAppointmentTime = fromDateTime; + break; + } + fromDateTime = fromDateTime.plusMinutes(15); + } + + if (validAppointmentTime != null) { + Appointment appointment = new Appointment().garageId(AggregateReference.to(garage.get().id())) + .appointmentTime(Date.from(validAppointmentTime.atZone(ZoneId.systemDefault()).toInstant())) + .serviceId(AggregateReference.to(service.get().id())).serviceCode(service.get().code()) + .serviceName(service.get().name()).duration(garageService.get().duration()); + + return Optional.of(repository.save(appointment)); + + } else { + return Optional.empty(); + } + } + + private boolean isSlotAvailable(Garage garage, LocalDateTime time) { + + long appointments = garage.appointments().stream() + .filter(a -> (a.appointmentStart().isBefore(time) || a.appointmentStart().isEqual(time)) + && a.appointmentEnd().isAfter(time)) + .count(); + + return appointments < garage.maxAppointments(); + } + + private LocalDateTime roundUpToQuarter(LocalDateTime datetime) { + + if (datetime.getMinute() % 15 == 0) + return datetime; + + int minutesToAdd = 15 - (datetime.getMinute() % 15); + return datetime.plusMinutes(minutesToAdd).truncatedTo(ChronoUnit.MINUTES); + } } diff --git a/src/main/java/de/etecture/ga/service/GarageService.java b/src/main/java/de/etecture/ga/service/GarageService.java new file mode 100644 index 0000000..19ff05a --- /dev/null +++ b/src/main/java/de/etecture/ga/service/GarageService.java @@ -0,0 +1,21 @@ +package de.etecture.ga.service; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import de.etecture.ga.model.Garage; +import de.etecture.ga.repository.GarageRepository; +import lombok.AllArgsConstructor; + +@Service +@AllArgsConstructor +public class GarageService { + + private final GarageRepository repository; + + + public Optional getGarage(long id) { + return repository.findById(id); + } +} diff --git a/src/main/java/de/etecture/ga/service/MDServiceService.java b/src/main/java/de/etecture/ga/service/MDServiceService.java index 37722f4..9261f9f 100644 --- a/src/main/java/de/etecture/ga/service/MDServiceService.java +++ b/src/main/java/de/etecture/ga/service/MDServiceService.java @@ -1,6 +1,7 @@ package de.etecture.ga.service; import java.security.InvalidParameterException; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; @@ -16,6 +17,11 @@ public class MDServiceService { private final MDServiceRepository serviceRepository; + + public Optional getMDService(long id) { + return serviceRepository.findById(id); + } + public MDService storeMDService(String serviceCode) { if (StringUtils.isBlank(serviceCode)) diff --git a/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java b/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java index 58988fd..a2d2631 100644 --- a/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java +++ b/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java @@ -1,18 +1,22 @@ package de.etecture.ga.api; +import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.fail; -import static org.hamcrest.Matchers.*; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.etecture.ga.dto.TerminRequest; import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Garage; import de.etecture.ga.repository.AppointmentRepository; @@ -114,7 +118,21 @@ class GarageApiControllerTest { } @Test - void testPostTermin() { - fail("Not yet implemented"); + void testPostTermin() throws Exception { + + TerminRequest terminReq = new TerminRequest().leistungsId("3").von("02.01.2019 12:00").bis("02.01.2019 13:00"); + + ObjectMapper objectMapper = new ObjectMapper(); + String terminJson = objectMapper.writeValueAsString(terminReq); + + this.mockMvc + .perform(MockMvcRequestBuilders.post("/werkstatt/{werkstattId}/termin", testGarage.id()) + .contentType(MediaType.APPLICATION_JSON).content(terminJson)) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.leistung").value("Radwechsel")) + .andExpect(MockMvcResultMatchers.jsonPath("$.leistungsId").value("3")) + .andExpect(MockMvcResultMatchers.jsonPath("$.von").value("02.01.2019 12:00")) + .andExpect(MockMvcResultMatchers.jsonPath("$.bis").value("02.01.2019 12:30")); } }