From a2164a0eb389725c06682e662369c0159ebad916 Mon Sep 17 00:00:00 2001 From: Matthias Engelien Date: Wed, 11 Sep 2024 22:43:38 +0200 Subject: [PATCH] Story 3 and Story 4 finished --- pom.xml | 4 + .../etecture/ga/api/GarageApiController.java | 63 ++++++++++-- .../dto/mapper/AppointmentTerminMapper.java | 32 ++++++ .../de/etecture/ga/model/Appointment.java | 3 + .../ga/repository/AppointmentRepository.java | 13 +++ .../ga/service/AppointmentService.java | 50 ++++++++++ .../ga/service/GarageImportService.java | 7 +- src/main/resources/schema.sql | 29 +++--- .../ga/api/GarageApiControllerTest.java | 99 ++++++++++++++++--- 9 files changed, 264 insertions(+), 36 deletions(-) create mode 100644 src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java create mode 100644 src/main/java/de/etecture/ga/service/AppointmentService.java diff --git a/pom.xml b/pom.xml index 21a3a2d..19720d7 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,10 @@ com.fasterxml.jackson.dataformat jackson-dataformat-csv + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + org.springdoc diff --git a/src/main/java/de/etecture/ga/api/GarageApiController.java b/src/main/java/de/etecture/ga/api/GarageApiController.java index 6110bdc..42cbbea 100644 --- a/src/main/java/de/etecture/ga/api/GarageApiController.java +++ b/src/main/java/de/etecture/ga/api/GarageApiController.java @@ -3,31 +3,68 @@ */ package de.etecture.ga.api; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.util.Assert; import de.etecture.ga.dto.Termin; import de.etecture.ga.dto.TerminRequest; +import de.etecture.ga.dto.mapper.AppointmentTerminMapper; +import de.etecture.ga.model.Appointment; +import de.etecture.ga.service.AppointmentService; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; /** * */ +@AllArgsConstructor +@Controller +@Slf4j public class GarageApiController implements WerkstattApi { + private final AppointmentService appointmentService; + @Override - public ResponseEntity getTermin(String werkstattId, String terminId) { - // TODO Auto-generated method stub - return WerkstattApi.super.getTermin(werkstattId, terminId); + public ResponseEntity getTermin(@NotNull String werkstattId, @NotNull String terminId) { + + Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); + Assert.isTrue(NumberUtils.isParsable(terminId), "terminId ungültig"); + + Optional appointment = appointmentService.getAppointment(Long.parseLong(terminId), + Long.parseLong(werkstattId)); + + return ResponseEntity.of(appointment.map(AppointmentTerminMapper::toTermin)); } @Override - public ResponseEntity> getTermine(String werkstattId, @Valid String von, @Valid String bis, + public ResponseEntity> getTermine(@NotNull String werkstattId, @Valid String von, @Valid String bis, @Valid String leistungsId) { - // TODO Auto-generated method stub - return WerkstattApi.super.getTermine(werkstattId, von, bis, leistungsId); + + Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); + + 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)); + + log.info("Filter appointments by garage {}, serviceId {}, from {}, till {}", garageId, serviceId, + appointmentsFrom, appointmentsTill); + + return ResponseEntity + .ok(appointmentService.getAppointments(garageId, serviceId, appointmentsFrom, appointmentsTill).stream() + .map(AppointmentTerminMapper::toTermin).toList()); } @Override @@ -43,4 +80,18 @@ public class GarageApiController implements WerkstattApi { return WerkstattApi.super.postTermin(werkstattId, termin); } + private Date parseLocalDateTime(String dateTimeString) { + + if (StringUtils.isNotBlank(dateTimeString)) { + try { + SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy hh:mm"); + + return format.parse(dateTimeString); + } catch (ParseException e) { + log.error("Invalid data to parse", e); + } + } + + return null; + } } diff --git a/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java new file mode 100644 index 0000000..96e4f9c --- /dev/null +++ b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java @@ -0,0 +1,32 @@ +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; +import de.etecture.ga.model.Appointment; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AppointmentTerminMapper { + + public static Termin toTermin(Appointment appointment) { + + 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"))); + + } +} diff --git a/src/main/java/de/etecture/ga/model/Appointment.java b/src/main/java/de/etecture/ga/model/Appointment.java index 092a8f8..acece4b 100644 --- a/src/main/java/de/etecture/ga/model/Appointment.java +++ b/src/main/java/de/etecture/ga/model/Appointment.java @@ -20,6 +20,9 @@ public class Appointment { @Column("GARAGE_ID") private AggregateReference garageId; + @Column("SERVICE_ID") + private AggregateReference serviceId; + private String serviceCode; private String serviceName; diff --git a/src/main/java/de/etecture/ga/repository/AppointmentRepository.java b/src/main/java/de/etecture/ga/repository/AppointmentRepository.java index cc4716e..84f6b58 100644 --- a/src/main/java/de/etecture/ga/repository/AppointmentRepository.java +++ b/src/main/java/de/etecture/ga/repository/AppointmentRepository.java @@ -1,9 +1,22 @@ package de.etecture.ga.repository; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.repository.CrudRepository; import de.etecture.ga.model.Appointment; public interface AppointmentRepository extends CrudRepository { + public Optional findByIdAndGarageId(long id, long garageId); + + public List findByGarageId(long garageId); + + @Query("select a.* from APPOINTMENT a " + + "join GARAGE g on g.ID = a.GARAGE_ID " + + "join MD_SERVICE s on s.ID = a.SERVICE_ID " + + "where s.CODE = :serviceCode and g.ID = :garageId ") + public List findByServiceCodeAndGarageId(String serviceCode, long garageId); } diff --git a/src/main/java/de/etecture/ga/service/AppointmentService.java b/src/main/java/de/etecture/ga/service/AppointmentService.java new file mode 100644 index 0000000..8e52d04 --- /dev/null +++ b/src/main/java/de/etecture/ga/service/AppointmentService.java @@ -0,0 +1,50 @@ +package de.etecture.ga.service; + +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import org.springframework.stereotype.Service; + +import de.etecture.ga.model.Appointment; +import de.etecture.ga.repository.AppointmentRepository; +import lombok.AllArgsConstructor; + +@Service +@AllArgsConstructor +public class AppointmentService { + + private final AppointmentRepository repository; + + public Optional getAppointment(long appointmentId) { + + return repository.findById(appointmentId); + + } + + public Optional getAppointment(long appointmentId, long garageId) { + + return repository.findByIdAndGarageId(appointmentId, garageId); + + } + + public List getAppointments(long garageId, Optional serviceId, Optional from, + Optional till) { + + Stream appointments = repository.findByGarageId(garageId).stream(); + + if (serviceId.isPresent()) { + appointments = appointments.filter(a -> serviceId.get().equals(a.serviceId().getId())); + } + if (from.isPresent()) { + appointments = appointments + .filter(a -> a.appointmentTime().equals(from.get()) || a.appointmentTime().after(from.get())); + } + if (till.isPresent()) { + appointments = appointments.filter(a -> a.appointmentTime().before(till.get())); + } + + return appointments.toList(); + } +} diff --git a/src/main/java/de/etecture/ga/service/GarageImportService.java b/src/main/java/de/etecture/ga/service/GarageImportService.java index 5d496c9..fc81380 100644 --- a/src/main/java/de/etecture/ga/service/GarageImportService.java +++ b/src/main/java/de/etecture/ga/service/GarageImportService.java @@ -15,9 +15,11 @@ import java.util.stream.Stream; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.dataformat.csv.CsvMapper; import com.fasterxml.jackson.dataformat.csv.CsvSchema; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Garage; @@ -88,6 +90,7 @@ public class GarageImportService { try { CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader(); CsvMapper mapper = new CsvMapper(); + MappingIterator readValues = mapper.readerFor(CSVData.class).with(bootstrapSchema) .readValues(file.toFile()); @@ -110,9 +113,9 @@ public class GarageImportService { private Appointment getAppointmentForService(MDService service, Date date, Long garageId) { return new Appointment().appointmentTime(date).serviceCode(service.code()).serviceName(service.name()) - .duration(service.duration()).garageId(AggregateReference.to(garageId)); + .duration(service.duration()).garageId(AggregateReference.to(garageId)).serviceId(AggregateReference.to(service.id())); } - private record CSVData(Date APP_DATE, String SERVICE) { + private record CSVData(@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm'Z'") Date APP_DATE, String SERVICE) { } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 594a810..c0b111a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -8,18 +8,6 @@ CREATE TABLE GARAGE ( PRIMARY KEY (ID) ); -CREATE TABLE APPOINTMENT ( - ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, - GARAGE_ID BIGINT NOT NULL, - SERVICE_CODE VARCHAR(5) DEFAULT NULL, - SERVICE_NAME VARCHAR(50) DEFAULT NULL, - APPOINTMENT_TIME DATE DEFAULT NULL, - SLOT INT NOT NULL DEFAULT 1, - DURATION BIGINT NOT NULL DEFAULT 0, - - PRIMARY KEY (ID), - FOREIGN KEY (GARAGE_ID) REFERENCES GARAGE(ID) -); CREATE TABLE MD_SERVICE ( ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, @@ -30,6 +18,7 @@ CREATE TABLE MD_SERVICE ( PRIMARY KEY (ID) ); + CREATE TABLE GARAGE_SERVICES ( GARAGE_ID BIGINT NOT NULL, SERVICE_ID BIGINT NOT NULL, @@ -39,4 +28,20 @@ CREATE TABLE GARAGE_SERVICES ( FOREIGN KEY (GARAGE_ID) REFERENCES GARAGE(ID), FOREIGN KEY (SERVICE_ID) REFERENCES MD_SERVICE(ID) +); + + +CREATE TABLE APPOINTMENT ( + ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE, + GARAGE_ID BIGINT NOT NULL, + SERVICE_ID BIGINT NOT NULL, + SERVICE_CODE VARCHAR(5) DEFAULT NULL, + SERVICE_NAME VARCHAR(50) DEFAULT NULL, + APPOINTMENT_TIME DATE DEFAULT NULL, + SLOT INT NOT NULL DEFAULT 1, + DURATION BIGINT NOT NULL DEFAULT 0, + + PRIMARY KEY (ID), + FOREIGN KEY (GARAGE_ID) REFERENCES GARAGE(ID), + FOREIGN KEY (SERVICE_ID) REFERENCES MD_SERVICE(ID) ); \ No newline at end of file diff --git a/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java b/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java index 6b27d58..58988fd 100644 --- a/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java +++ b/src/test/java/de/etecture/ga/api/GarageApiControllerTest.java @@ -1,18 +1,23 @@ package de.etecture.ga.api; -import static org.junit.jupiter.api.Assertions.assertEquals; 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.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; 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 de.etecture.ga.model.Appointment; +import de.etecture.ga.model.Garage; +import de.etecture.ga.repository.AppointmentRepository; +import de.etecture.ga.repository.GarageRepository; + /** * Integrationtest for the {@link WerkstattApi} */ @@ -22,22 +27,85 @@ class GarageApiControllerTest { @Autowired private MockMvc mockMvc; - - @Test - void testGetTermin() throws Exception { - - MvcResult mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", "1", "1")) - .andDo(MockMvcResultHandlers.print()) - .andExpect(MockMvcResultMatchers.status().isOk()) -// .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Hello World!!!")) - .andReturn(); - - assertEquals("application/json;charset=UTF-8", mvcResult.getResponse().getContentType()); + + @Autowired + private GarageRepository garageRepository; + + @Autowired + private AppointmentRepository appointmentRepository; + + private Garage testGarage; + + private Appointment testAppointment; + + @BeforeEach + void setupData() { + + testGarage = garageRepository.findByCode("test-data").get(); + testAppointment = appointmentRepository.findByServiceCodeAndGarageId("WHE", testGarage.id()).get(0); } @Test - void testGetTermine() { - fail("Not yet implemented"); + void testGetTermin() throws Exception { + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", testGarage.id(), + testAppointment.id())) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1")) + .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")); + + } + + @Test + void testGetInvalidTermin() throws Exception { + + this.mockMvc + .perform( + MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", testGarage.id(), "20")) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isNotFound()); + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", 2, + testAppointment.id())) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isNotFound()); + + } + + @Test + void testGetTermine() throws Exception { + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()).param("thing", + "somewhere")) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(19))); + } + + @Test + void testGetTermineWithFilter() throws Exception { + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) + .param("leistungsId", "3")) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(7))); + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) + .param("leistungsId", "3").param("von", "05.01.2019 12:00")) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(5))); + + this.mockMvc + .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) + .param("leistungsId", "3").param("von", "05.01.2019 12:00").param("bis", "08.01.2019 12:00")) + .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(1))); + } @Test @@ -49,5 +117,4 @@ class GarageApiControllerTest { void testPostTermin() { fail("Not yet implemented"); } - }