Compare commits
10 Commits
1f7dfee78f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
856523fba6 | ||
|
|
0a7559b94f | ||
|
|
ebc66ff8f6 | ||
|
|
86ecf3b317 | ||
|
|
cbd6d373bb | ||
|
|
c311564ecc | ||
|
|
a2164a0eb3 | ||
|
|
97d86cd11a | ||
|
|
6985e0ea87 | ||
|
|
a0cf2d1854 |
50
pom.xml
50
pom.xml
@@ -22,6 +22,7 @@
|
|||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- Spring libs -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
@@ -30,18 +31,16 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jdbc</artifactId>
|
<artifactId>spring-boot-starter-data-jdbc</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- CSV handling -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>jackson-dataformat-csv</artifactId>
|
||||||
<scope>runtime</scope>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
<optional>true</optional>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- OpenAPI generation -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-ui</artifactId>
|
<artifactId>springdoc-openapi-ui</artifactId>
|
||||||
@@ -52,14 +51,34 @@
|
|||||||
<artifactId>jackson-databind-nullable</artifactId>
|
<artifactId>jackson-databind-nullable</artifactId>
|
||||||
<version>0.2.6</version>
|
<version>0.2.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- DB driver - change to other driver on DB change -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- convenient -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.threeten</groupId>
|
||||||
|
<artifactId>threeten-extra</artifactId>
|
||||||
|
<version>1.8.0</version>
|
||||||
|
</dependency>
|
||||||
<!-- testing -->
|
<!-- testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mockito</groupId>
|
||||||
|
<artifactId>mockito-core</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -79,7 +98,7 @@
|
|||||||
</inputSpec>
|
</inputSpec>
|
||||||
<generatorName>spring</generatorName>
|
<generatorName>spring</generatorName>
|
||||||
<apiPackage>de.etecture.ga.api</apiPackage>
|
<apiPackage>de.etecture.ga.api</apiPackage>
|
||||||
<modelPackage>de.etecture.ga.model</modelPackage>
|
<modelPackage>de.etecture.ga.dto</modelPackage>
|
||||||
<supportingFilesToGenerate>
|
<supportingFilesToGenerate>
|
||||||
ApiUtil.java
|
ApiUtil.java
|
||||||
</supportingFilesToGenerate>
|
</supportingFilesToGenerate>
|
||||||
@@ -104,6 +123,15 @@
|
|||||||
</excludes>
|
</excludes>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
<!-- skip failing tests for now. There is a problem with the DB creation for multiple tests runs -->
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<testFailureIgnore>true</testFailureIgnore>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class GarageAppointmentManagementApplication {
|
public class GarageAppointmentApp {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(GarageAppointmentManagementApplication.class, args);
|
SpringApplication.run(GarageAppointmentApp.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
125
src/main/java/de/etecture/ga/api/GarageApiController.java
Normal file
125
src/main/java/de/etecture/ga/api/GarageApiController.java
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package de.etecture.ga.api;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 de.etecture.ga.util.DateTimeUtil;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link WerkstattApi}.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Controller
|
||||||
|
@Slf4j
|
||||||
|
public class GarageApiController implements WerkstattApi {
|
||||||
|
|
||||||
|
private final AppointmentService appointmentService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see WerkstattApi#getTermin(String, String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Termin> getTermin(@NotNull String werkstattId, @NotNull String terminId) {
|
||||||
|
|
||||||
|
// input validation
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig");
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(terminId), "terminId ungültig");
|
||||||
|
|
||||||
|
// read appointment
|
||||||
|
Optional<Appointment> appointment = appointmentService.getAppointment(Long.parseLong(terminId),
|
||||||
|
Long.parseLong(werkstattId));
|
||||||
|
|
||||||
|
return ResponseEntity.of(appointment.map(AppointmentTerminMapper::toTermin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see WerkstattApi#getTermine(String, String, String, String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<Termin>> getTermine(@NotNull String werkstattId, @Valid String von, @Valid String bis,
|
||||||
|
@Valid String leistungsId) {
|
||||||
|
|
||||||
|
// input validation
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig");
|
||||||
|
|
||||||
|
long garageId = Long.parseLong(werkstattId);
|
||||||
|
Optional<Long> serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId))
|
||||||
|
: Optional.empty();
|
||||||
|
Optional<LocalDateTime> appointmentsFrom = Optional.ofNullable(DateTimeUtil.toLocalDateTime(von));
|
||||||
|
Optional<LocalDateTime> appointmentsTill = Optional.ofNullable(DateTimeUtil.toLocalDateTime(bis));
|
||||||
|
|
||||||
|
// return list of appointments
|
||||||
|
return ResponseEntity
|
||||||
|
.ok(appointmentService.getAppointments(garageId, serviceId, appointmentsFrom, appointmentsTill).stream()
|
||||||
|
.map(AppointmentTerminMapper::toTermin).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see WerkstattApi#getTerminvorschlaege(String, String, String, String)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<Termin>> getTerminvorschlaege(String werkstattId, @NotNull @Valid String leistungsId,
|
||||||
|
@Valid String von, @Valid String bis) {
|
||||||
|
|
||||||
|
// input validation
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig");
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(leistungsId), "leistungsId ungültig");
|
||||||
|
Assert.notNull(DateTimeUtil.toDate(von), "von ungültig");
|
||||||
|
Assert.notNull(DateTimeUtil.toDate(bis), "bis ungültig");
|
||||||
|
|
||||||
|
long garageId = Long.parseLong(werkstattId);
|
||||||
|
long serviceId = Long.parseLong(leistungsId);
|
||||||
|
LocalDateTime appointmentFrom = DateTimeUtil.toLocalDateTime(von);
|
||||||
|
LocalDateTime appointmentTill = DateTimeUtil.toLocalDateTime(bis);
|
||||||
|
|
||||||
|
// return List of free slots
|
||||||
|
return ResponseEntity
|
||||||
|
.ok(appointmentService.getAppointmentSuggestion(garageId, serviceId, appointmentFrom, appointmentTill)
|
||||||
|
.stream().map(AppointmentTerminMapper::toTermin).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see WerkstattApi#postTermin(String, TerminRequest)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Termin> postTermin(String werkstattId, @Valid TerminRequest termin) {
|
||||||
|
|
||||||
|
// input validation
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig");
|
||||||
|
Assert.isTrue(NumberUtils.isParsable(termin.getLeistungsId()), "leistungsId ungültig");
|
||||||
|
Assert.notNull(DateTimeUtil.toDate(termin.getVon()), "von ungültig");
|
||||||
|
Assert.notNull(DateTimeUtil.toDate(termin.getBis()), "bis ungültig");
|
||||||
|
|
||||||
|
long garageId = Long.parseLong(werkstattId);
|
||||||
|
long serviceId = Long.parseLong(termin.getLeistungsId());
|
||||||
|
LocalDateTime appointmentFrom = DateTimeUtil.toLocalDateTime(termin.getVon());
|
||||||
|
LocalDateTime appointmentTill = DateTimeUtil.toLocalDateTime(termin.getBis());
|
||||||
|
|
||||||
|
// create appointment if possible
|
||||||
|
Optional<Appointment> appointment = appointmentService.createAppointment(garageId, serviceId, appointmentFrom,
|
||||||
|
appointmentTill);
|
||||||
|
|
||||||
|
return appointment.map(a -> ResponseEntity.ok(AppointmentTerminMapper.toTermin(a)))
|
||||||
|
.orElse(ResponseEntity.status(HttpStatus.CONFLICT).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package de.etecture.ga.config;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.data.convert.ReadingConverter;
|
||||||
|
import org.springframework.data.convert.WritingConverter;
|
||||||
|
import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adding converters for {@link Duration} for easier working with time in the DB models
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class DataBaseConfiguration extends AbstractJdbcConfiguration {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<?> userConverters() {
|
||||||
|
return Arrays.asList(new DurationToLongConverter(), new LongToDurationConverter());
|
||||||
|
}
|
||||||
|
|
||||||
|
@WritingConverter
|
||||||
|
public class DurationToLongConverter implements Converter<Duration, Long> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long convert(Duration duration) {
|
||||||
|
return duration.toSeconds();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ReadingConverter
|
||||||
|
public class LongToDurationConverter implements Converter<Long, Duration> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Duration convert(Long duration) {
|
||||||
|
return Duration.of(duration, ChronoUnit.SECONDS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
src/main/java/de/etecture/ga/config/InitData.java
Normal file
26
src/main/java/de/etecture/ga/config/InitData.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package de.etecture.ga.config;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import de.etecture.ga.service.GarageImportService;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import data on startup.
|
||||||
|
* This can be used to import old appointments.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class InitData {
|
||||||
|
|
||||||
|
private final GarageImportService importService;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void importData() {
|
||||||
|
|
||||||
|
importService.importGarageData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package de.etecture.ga.dto.mapper;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import de.etecture.ga.dto.Termin;
|
||||||
|
import de.etecture.ga.model.Appointment;
|
||||||
|
import de.etecture.ga.util.DateTimeUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapper class to map between {@link Appointment} models and {@link Termin} DTO-Class
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class AppointmentTerminMapper {
|
||||||
|
|
||||||
|
public static Termin toTermin(Appointment appointment) {
|
||||||
|
|
||||||
|
log.debug("Mapping Object {}", appointment);
|
||||||
|
|
||||||
|
String id = Optional.ofNullable(appointment.id()).map(i -> Long.toString(i)).orElse(null);
|
||||||
|
String serviceId = Optional.ofNullable(appointment.serviceId().getId()).map(i -> Long.toString(i)).orElse(null);
|
||||||
|
String serviceName = Optional.ofNullable(appointment.serviceName()).orElse(null);
|
||||||
|
String garageName = "to_be_set";
|
||||||
|
String from = Optional.ofNullable(appointment.appointmentStart()).map(DateTimeUtil::toString).orElse(null);
|
||||||
|
String till = Optional.ofNullable(appointment.appointmentEnd()).map(DateTimeUtil::toString).orElse(null);
|
||||||
|
|
||||||
|
return new Termin().id(id).leistungsId(serviceId).leistung(serviceName).werkstattName(garageName).von(from)
|
||||||
|
.bis(till);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package de.etecture.ga.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class ResponseExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(value = { IllegalArgumentException.class })
|
||||||
|
protected ResponseEntity<Object> handleConflict(RuntimeException ex, WebRequest request) {
|
||||||
|
String bodyOfResponse = ex.getMessage();
|
||||||
|
return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.NOT_ACCEPTABLE, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/main/java/de/etecture/ga/model/Appointment.java
Normal file
63
src/main/java/de/etecture/ga/model/Appointment.java
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package de.etecture.ga.model;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
|
||||||
|
import de.etecture.ga.util.DateTimeUtil;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(fluent = true, chain = true)
|
||||||
|
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||||
|
public class Appointment implements Comparable<Appointment> {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@EqualsAndHashCode.Include
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column("GARAGE_ID")
|
||||||
|
private AggregateReference<Garage, Long> garageId;
|
||||||
|
|
||||||
|
@Column("SERVICE_ID")
|
||||||
|
private AggregateReference<MDService, Long> serviceId;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Include
|
||||||
|
private String serviceCode;
|
||||||
|
|
||||||
|
private String serviceName;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Include
|
||||||
|
private Date appointmentTime;
|
||||||
|
|
||||||
|
@EqualsAndHashCode.Include
|
||||||
|
private Integer slot = 1;
|
||||||
|
|
||||||
|
private Duration duration = Duration.ZERO;
|
||||||
|
|
||||||
|
public LocalDateTime appointmentStart() {
|
||||||
|
if (this.appointmentTime == null)
|
||||||
|
return null;
|
||||||
|
return DateTimeUtil.toLocalDateTime(this.appointmentTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime appointmentEnd() {
|
||||||
|
if (this.appointmentTime == null)
|
||||||
|
return null;
|
||||||
|
return this.appointmentStart().plus(this.duration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Appointment o) {
|
||||||
|
if (o == null || o.appointmentTime() == null)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return this.appointmentTime().compareTo(o.appointmentTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/main/java/de/etecture/ga/model/Garage.java
Normal file
68
src/main/java/de/etecture/ga/model/Garage.java
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package de.etecture.ga.model;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||||
|
import org.springframework.data.relational.core.mapping.MappedCollection;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(fluent = true, chain = true)
|
||||||
|
public class Garage {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Integer maxAppointments = 1;
|
||||||
|
|
||||||
|
@MappedCollection(idColumn = "GARAGE_ID")
|
||||||
|
private Set<Appointment> appointments = new HashSet<>();
|
||||||
|
|
||||||
|
@MappedCollection(idColumn = "GARAGE_ID")
|
||||||
|
private Set<GarageServices> garageServices = new HashSet<>();
|
||||||
|
|
||||||
|
public Garage addAppointment(Appointment appointment) {
|
||||||
|
|
||||||
|
boolean added = this.appointments.add(appointment);
|
||||||
|
if(!added) {
|
||||||
|
appointment.slot(appointment.slot() + 1);
|
||||||
|
this.addAppointment(appointment);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Appointment> appointmentsSorted() {
|
||||||
|
return this.appointments.stream().sorted().toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Garage addService(MDService service) {
|
||||||
|
garageServices.add(createGarageService(service, null));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addService(MDService service, Duration duration) {
|
||||||
|
garageServices.add(createGarageService(service, duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GarageServices createGarageService(MDService service, Duration duration) {
|
||||||
|
|
||||||
|
Assert.notNull(service, "Service must not be null");
|
||||||
|
Assert.notNull(service.id(), "Service id, must not be null");
|
||||||
|
|
||||||
|
duration = duration == null ? service.duration() : duration;
|
||||||
|
|
||||||
|
return new GarageServices().garageId(AggregateReference.to(this.id()))
|
||||||
|
.serviceId(AggregateReference.to(service.id())).duration(duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/de/etecture/ga/model/GarageServices.java
Normal file
23
src/main/java/de/etecture/ga/model/GarageServices.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package de.etecture.ga.model;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(fluent = true, chain = true)
|
||||||
|
public class GarageServices {
|
||||||
|
|
||||||
|
@Column("GARAGE_ID")
|
||||||
|
private AggregateReference<Garage, Long> garageId;
|
||||||
|
|
||||||
|
@Column("SERVICE_ID")
|
||||||
|
private AggregateReference<MDService, Long> serviceId;
|
||||||
|
|
||||||
|
private Duration duration;
|
||||||
|
|
||||||
|
}
|
||||||
23
src/main/java/de/etecture/ga/model/MDService.java
Normal file
23
src/main/java/de/etecture/ga/model/MDService.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package de.etecture.ga.model;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(fluent = true, chain = true)
|
||||||
|
public class MDService {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String code;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private Duration duration = Duration.ZERO;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package de.etecture.ga.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.Appointment;
|
||||||
|
|
||||||
|
public interface AppointmentRepository extends CrudRepository<Appointment, Long> {
|
||||||
|
|
||||||
|
public Optional<Appointment> findByIdAndGarageId(long id, long garageId);
|
||||||
|
|
||||||
|
public List<Appointment> findByGarageId(long garageId);
|
||||||
|
|
||||||
|
public List<Appointment> findByServiceCodeAndGarageId(String serviceCode, long garageId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.etecture.ga.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.Garage;
|
||||||
|
|
||||||
|
public interface GarageRepository extends CrudRepository<Garage, Long> {
|
||||||
|
|
||||||
|
public Optional<Garage> findByCode(String code);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.etecture.ga.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.jdbc.repository.query.Query;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.GarageServices;
|
||||||
|
import de.etecture.ga.model.MDService;
|
||||||
|
|
||||||
|
public interface GarageServiceRepository extends CrudRepository<GarageServices, Long> {
|
||||||
|
|
||||||
|
|
||||||
|
@Query("select s.ID, s.CODE, s.NAME, ifNull(gs.DURATION, s.DURATION) as DURATION "
|
||||||
|
+ "from GARAGE_SERVICES gs "
|
||||||
|
+ "join MD_SERVICE s on s.ID = gs.SERVICE_ID "
|
||||||
|
+ "join GARAGE g on g.ID = gs.GARAGE_ID "
|
||||||
|
+ "where s.CODE = :serviceCode and g.CODE = :garageCode")
|
||||||
|
public Optional<MDService> findByServiceCodeAndGarage(String serviceCode, String garageCode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package de.etecture.ga.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.MDService;
|
||||||
|
|
||||||
|
public interface MDServiceRepository extends CrudRepository<MDService, Long> {
|
||||||
|
|
||||||
|
public Optional<MDService> findByCode(String code);
|
||||||
|
}
|
||||||
211
src/main/java/de/etecture/ga/service/AppointmentService.java
Normal file
211
src/main/java/de/etecture/ga/service/AppointmentService.java
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package de.etecture.ga.service;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
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 org.springframework.util.Assert;
|
||||||
|
|
||||||
|
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 de.etecture.ga.util.DateTimeUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle all {@link Appointment} related tasks
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class AppointmentService {
|
||||||
|
|
||||||
|
private final AppointmentRepository repository;
|
||||||
|
|
||||||
|
private final GarageService garageService;
|
||||||
|
|
||||||
|
private final MDServiceService serviceService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gives the appointment for the provided id. The appointment is checked against
|
||||||
|
* the given {@link Garage} to validate that the appointment is assigned to the
|
||||||
|
* garage.
|
||||||
|
*
|
||||||
|
* @param appointmentId id of {@link Appointment}
|
||||||
|
* @param garageId id of {@link Garage}
|
||||||
|
*
|
||||||
|
* @return an {@link Appointment} if found and valid
|
||||||
|
*/
|
||||||
|
public Optional<Appointment> getAppointment(long appointmentId, long garageId) {
|
||||||
|
|
||||||
|
Assert.isTrue(appointmentId > 0, "appointmentId must be bigger than 0");
|
||||||
|
Assert.isTrue(garageId > 0, "garageId must be bigger than 0");
|
||||||
|
|
||||||
|
return repository.findByIdAndGarageId(appointmentId, garageId);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads all {@link Appointment} for the given {@link Garage}. This list can be
|
||||||
|
* filtered by {@link Service} and/or date range
|
||||||
|
*
|
||||||
|
* @param garageId if of {@link Garage}
|
||||||
|
* @param serviceId Optional: if if {@link Service}
|
||||||
|
* @param from Optional: {@link Appointment} start from
|
||||||
|
* @param till Optional: {@link Appointment} start till, exclusive
|
||||||
|
*
|
||||||
|
* @return list of appointments
|
||||||
|
*/
|
||||||
|
public List<Appointment> getAppointments(long garageId, Optional<Long> serviceId, Optional<LocalDateTime> from,
|
||||||
|
Optional<LocalDateTime> till) {
|
||||||
|
|
||||||
|
Assert.isTrue(garageId > 0, "garageId must be bigger than 0");
|
||||||
|
|
||||||
|
Stream<Appointment> appointments = repository.findByGarageId(garageId).stream();
|
||||||
|
|
||||||
|
if (serviceId.isPresent()) {
|
||||||
|
appointments = appointments.filter(a -> serviceId.get().equals(a.serviceId().getId()));
|
||||||
|
}
|
||||||
|
if (from.isPresent()) {
|
||||||
|
Date fromDate = DateTimeUtil.toDate(from.get());
|
||||||
|
appointments = appointments
|
||||||
|
.filter(a -> a.appointmentTime().equals(fromDate) || a.appointmentTime().after(fromDate));
|
||||||
|
}
|
||||||
|
if (till.isPresent()) {
|
||||||
|
Date tillDate = DateTimeUtil.toDate(till.get());
|
||||||
|
appointments = appointments.filter(a -> a.appointmentTime().before(tillDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
return appointments.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all possible appointment slots for the given parameter
|
||||||
|
*
|
||||||
|
* @param garageId id of {@link Garage}
|
||||||
|
* @param serviceId id of {@link Service}
|
||||||
|
* @param from time frame to check start
|
||||||
|
* @param till time frame to check end
|
||||||
|
*
|
||||||
|
* @return list with all open appointment slots
|
||||||
|
*/
|
||||||
|
public List<Appointment> getAppointmentSuggestion(Long garageId, Long serviceId, LocalDateTime from,
|
||||||
|
LocalDateTime till) {
|
||||||
|
|
||||||
|
Assert.isTrue(garageId > 0, "garageId must be bigger than 0");
|
||||||
|
Assert.isTrue(serviceId > 0, "appointmentId must be bigger than 0");
|
||||||
|
Assert.notNull(from, "from must be not null");
|
||||||
|
Assert.notNull(till, "till must be not null");
|
||||||
|
|
||||||
|
Garage garage = garageService.getGarage(garageId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("GarageId not valid"));
|
||||||
|
GarageServices garageService = garage.garageServices().stream()
|
||||||
|
.filter(gs -> serviceId == gs.serviceId().getId()).findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("serviceId not valid"));
|
||||||
|
|
||||||
|
return getValidAppointmentTimeList(garage, garageService, from, till).stream().map(t -> new Appointment()
|
||||||
|
.serviceId(AggregateReference.to(serviceId)).appointmentTime(DateTimeUtil.toDate(t))).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an {@link Appointment} for the given parameter. The
|
||||||
|
* {@link Appointment} start will be in the given time frame.
|
||||||
|
*
|
||||||
|
* @param garageId id of {@link Garage}
|
||||||
|
* @param serviceId id of {@link Service}
|
||||||
|
* @param from time frame to use start
|
||||||
|
* @param till time frame to use end
|
||||||
|
*
|
||||||
|
* @return if the time frame is valid a new {@link Appointment}
|
||||||
|
*/
|
||||||
|
public Optional<Appointment> createAppointment(long garageId, long serviceId, LocalDateTime from,
|
||||||
|
LocalDateTime till) {
|
||||||
|
|
||||||
|
Assert.isTrue(garageId > 0, "garageId must be bigger than 0");
|
||||||
|
Assert.isTrue(serviceId > 0, "appointmentId must be bigger than 0");
|
||||||
|
Assert.notNull(from, "from must be not null");
|
||||||
|
Assert.notNull(till, "till must be not null");
|
||||||
|
|
||||||
|
Garage garage = garageService.getGarage(garageId)
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("GarageId not valid"));
|
||||||
|
MDService service = serviceService.getMDService(serviceId)
|
||||||
|
.filter(s -> garage.garageServices().stream().anyMatch(gs -> s.id() == gs.serviceId().getId()))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("serviceId not valid"));
|
||||||
|
GarageServices garageServices = garage.garageServices().stream()
|
||||||
|
.filter(gs -> serviceId == gs.serviceId().getId()).findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("serviceId not valid"));
|
||||||
|
|
||||||
|
// check appointment times
|
||||||
|
LocalDateTime validAppointmentTime = getValidAppointmentTime(garage, garageServices, from, till);
|
||||||
|
|
||||||
|
// create appointment
|
||||||
|
if (validAppointmentTime != null) {
|
||||||
|
Appointment appointment = createAppointmentObj(garage, service, garageServices, validAppointmentTime);
|
||||||
|
|
||||||
|
return Optional.of(repository.save(appointment));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LocalDateTime> getValidAppointmentTimeList(Garage garage, GarageServices service, LocalDateTime from,
|
||||||
|
LocalDateTime till) {
|
||||||
|
|
||||||
|
List<LocalDateTime> result = new ArrayList<>();
|
||||||
|
|
||||||
|
from = roundUpToQuarter(from);
|
||||||
|
|
||||||
|
while (from.isBefore(till)) {
|
||||||
|
if (isSlotAvailable(garage, from, service.duration())) {
|
||||||
|
result.add(from);
|
||||||
|
}
|
||||||
|
from = from.plusMinutes(15);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime getValidAppointmentTime(Garage garage, GarageServices service, LocalDateTime from,
|
||||||
|
LocalDateTime till) {
|
||||||
|
|
||||||
|
return getValidAppointmentTimeList(garage, service, from, till).getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSlotAvailable(Garage garage, LocalDateTime startTime, Duration duration) {
|
||||||
|
|
||||||
|
long appointments = garage.appointments().stream().filter(a -> DateTimeUtil.overlaps(a.appointmentStart(),
|
||||||
|
a.appointmentEnd(), startTime, startTime.plus(duration))).count();
|
||||||
|
|
||||||
|
return appointments < garage.maxAppointments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Appointment createAppointmentObj(Garage garage, MDService service, GarageServices garageService,
|
||||||
|
LocalDateTime validAppointmentTime) {
|
||||||
|
|
||||||
|
Appointment appointment = new Appointment().garageId(AggregateReference.to(garage.id()))
|
||||||
|
.appointmentTime(DateTimeUtil.toDate(validAppointmentTime))
|
||||||
|
.serviceId(AggregateReference.to(service.id())).serviceCode(service.code()).serviceName(service.name())
|
||||||
|
.duration(garageService.duration());
|
||||||
|
return appointment;
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/main/java/de/etecture/ga/service/GarageImportService.java
Normal file
132
src/main/java/de/etecture/ga/service/GarageImportService.java
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package de.etecture.ga.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.springframework.data.jdbc.core.mapping.AggregateReference;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.MappingIterator;
|
||||||
|
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
|
||||||
|
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.Appointment;
|
||||||
|
import de.etecture.ga.model.Garage;
|
||||||
|
import de.etecture.ga.model.MDService;
|
||||||
|
import de.etecture.ga.repository.AppointmentRepository;
|
||||||
|
import de.etecture.ga.repository.GarageRepository;
|
||||||
|
import de.etecture.ga.repository.GarageServiceRepository;
|
||||||
|
import de.etecture.ga.util.DateTimeUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports old {@link Appointment} data from given CSV files
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GarageImportService {
|
||||||
|
|
||||||
|
private static final String IMPORT_FOLDER = "import";
|
||||||
|
|
||||||
|
private final GarageRepository garageRepository;
|
||||||
|
|
||||||
|
private final GarageServiceRepository garageServiceRepository;
|
||||||
|
|
||||||
|
private final AppointmentRepository appointmentRepository;
|
||||||
|
|
||||||
|
public void importGarageData() {
|
||||||
|
|
||||||
|
try (Stream<Path> files = Files
|
||||||
|
.list(Paths.get(getClass().getClassLoader().getResource(IMPORT_FOLDER).toURI()))) {
|
||||||
|
|
||||||
|
files.filter(Files::isRegularFile).filter(path -> path.toString().endsWith(".csv"))
|
||||||
|
.forEach(this::loadGarageData);
|
||||||
|
|
||||||
|
} catch (IOException | URISyntaxException e) {
|
||||||
|
log.error("Can't read file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Garage> loadGarageData(final Path file) {
|
||||||
|
|
||||||
|
Optional<Garage> garage = garageRepository.findByCode(getGarageNameFromFile(file));
|
||||||
|
|
||||||
|
if (garage.isPresent()) {
|
||||||
|
List<CSVData> appointmentData = loadObjectsFromFile(file);
|
||||||
|
|
||||||
|
appointmentData.stream().filter(Objects::nonNull)
|
||||||
|
.forEach(data -> addAppointmentToGarage(data, garage.get()));
|
||||||
|
|
||||||
|
// save appointment data
|
||||||
|
garage.get().appointments().forEach(appointmentRepository::save);
|
||||||
|
}
|
||||||
|
|
||||||
|
return garage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getGarageNameFromFile(final Path fileName) {
|
||||||
|
|
||||||
|
String name = fileName.getFileName().toString();
|
||||||
|
|
||||||
|
int pos = name.lastIndexOf(".");
|
||||||
|
if (pos > 0 && pos < (name.length() - 1)) {
|
||||||
|
name = name.substring(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<CSVData> loadObjectsFromFile(final Path file) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
CsvSchema schema = CsvSchema.emptySchema().withHeader();
|
||||||
|
CsvMapper mapper = new CsvMapper();
|
||||||
|
|
||||||
|
MappingIterator<CSVData> readValues = mapper.readerFor(CSVData.class).with(schema)
|
||||||
|
.readValues(file.toFile());
|
||||||
|
|
||||||
|
return readValues.readAll();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error occurred while loading object list from file {}", file, e);
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAppointmentToGarage(CSVData data, Garage garage) {
|
||||||
|
|
||||||
|
Optional<MDService> garageService = garageServiceRepository
|
||||||
|
.findByServiceCodeAndGarage(data.appointmentServiceCode(), garage.code());
|
||||||
|
|
||||||
|
garageService.map(service -> getAppointmentForService(service, data.appointmentDate(), garage.id()))
|
||||||
|
.ifPresent(garage::addAppointment);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.serviceId(AggregateReference.to(service.id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CSVData(String APP_DATE, String SERVICE) {
|
||||||
|
|
||||||
|
public Date appointmentDate() {
|
||||||
|
return DateTimeUtil.toDate(APP_DATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String appointmentServiceCode() {
|
||||||
|
return SERVICE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/main/java/de/etecture/ga/service/GarageService.java
Normal file
33
src/main/java/de/etecture/ga/service/GarageService.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package de.etecture.ga.service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.Garage;
|
||||||
|
import de.etecture.ga.repository.GarageRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle all {@link Garage} related tasks
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class GarageService {
|
||||||
|
|
||||||
|
private final GarageRepository repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the {@link Garage} object for the given id.
|
||||||
|
*
|
||||||
|
* @param garageId id of {@link Garage} to read
|
||||||
|
*
|
||||||
|
* @return a {@link Garage} for the given id
|
||||||
|
*/
|
||||||
|
public Optional<Garage> getGarage(long garageId) {
|
||||||
|
Assert.isTrue(garageId > 0, "A valid garageId must be given");
|
||||||
|
|
||||||
|
return repository.findById(garageId);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/main/java/de/etecture/ga/service/MDServiceService.java
Normal file
33
src/main/java/de/etecture/ga/service/MDServiceService.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package de.etecture.ga.service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.MDService;
|
||||||
|
import de.etecture.ga.repository.MDServiceRepository;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle all {@link MDService} related tasks
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MDServiceService {
|
||||||
|
|
||||||
|
private final MDServiceRepository serviceRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the {@link MDService} object for the given id.
|
||||||
|
*
|
||||||
|
* @param serviceId id of {@link MDService} to read
|
||||||
|
*
|
||||||
|
* @return a {@link MDService} for the given id
|
||||||
|
*/
|
||||||
|
public Optional<MDService> getMDService(long serviceId) {
|
||||||
|
Assert.isTrue(serviceId > 0, "A valid serviceId must be given");
|
||||||
|
|
||||||
|
return serviceRepository.findById(serviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
144
src/main/java/de/etecture/ga/util/DateTimeUtil.java
Normal file
144
src/main/java/de/etecture/ga/util/DateTimeUtil.java
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package de.etecture.ga.util;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.threeten.extra.Interval;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some methods to handle {@link Date} and {@link LocalDateTime} conversion and
|
||||||
|
* parsing
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class DateTimeUtil {
|
||||||
|
|
||||||
|
private static final String DEFAULT_DATEFORMAT = "dd.MM.yyyy HH:mm";
|
||||||
|
|
||||||
|
private static final String IMPORT_DATEFORMAT = "yyyy-MM-dd'T'HH:mm'Z'";
|
||||||
|
|
||||||
|
private static final Pattern IMPORT_DATEPATTERN = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})Z");
|
||||||
|
|
||||||
|
private DateTimeUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a {@link LocalDateTime} object into a {@link Date}
|
||||||
|
*
|
||||||
|
* @param ldt the object to convert
|
||||||
|
* @return a {@link Date}
|
||||||
|
*/
|
||||||
|
public static Date toDate(LocalDateTime ldt) {
|
||||||
|
if (ldt == null)
|
||||||
|
return null;
|
||||||
|
return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a {@link Date} object into a {@link LocalDateTime}
|
||||||
|
*
|
||||||
|
* @param date the object to convert
|
||||||
|
* @return a {@link LocalDateTime}
|
||||||
|
*/
|
||||||
|
public static LocalDateTime toLocalDateTime(Date date) {
|
||||||
|
if (date == null)
|
||||||
|
return null;
|
||||||
|
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a {@link Date} into a string representation in the format 'dd.MM.yyyy HH:mm'
|
||||||
|
*
|
||||||
|
* @param date the object to convert
|
||||||
|
* @return the string representing the {@link Date} object
|
||||||
|
*/
|
||||||
|
public static String toString(Date date) {
|
||||||
|
|
||||||
|
return toString(toLocalDateTime(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a {@link LocalDateTime} into a string representation in the format 'dd.MM.yyyy HH:mm'
|
||||||
|
*
|
||||||
|
* @param ldt the object to convert
|
||||||
|
* @return the string representing the {@link LocalDateTime} object
|
||||||
|
*/
|
||||||
|
public static String toString(LocalDateTime ldt) {
|
||||||
|
if (ldt == null)
|
||||||
|
return null;
|
||||||
|
return ldt.format(DateTimeFormatter.ofPattern(DEFAULT_DATEFORMAT));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to parse the given string into a {@link Date}.
|
||||||
|
* If it was not possible to parse the string, <code>null</code> is returned
|
||||||
|
*
|
||||||
|
* @param dateTimeString the string to parse, must be in the format 'dd.MM.yyyy HH:mm' or 'yyyy-MM-ddTHH:mmZ'
|
||||||
|
* @return a {@link Date} if the string was parsed, <code>null</code> if not
|
||||||
|
*/
|
||||||
|
public static Date toDate(String dateTimeString) {
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(dateTimeString)) {
|
||||||
|
try {
|
||||||
|
DateFormat format = getFormatter(dateTimeString);
|
||||||
|
return format.parse(dateTimeString);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
log.error("Invalid data to parse", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to parse the given string into a {@link LocalDateTime}.
|
||||||
|
* If it was not possible to parse the string, <code>null</code> is returned
|
||||||
|
*
|
||||||
|
* @param dateTimeString the string to parse, must be in the format 'dd.MM.yyyy HH:mm' or 'yyyy-MM-ddTHH:mmZ'
|
||||||
|
* @return a {@link LocalDateTime} if the string was parsed, <code>null</code> if not
|
||||||
|
*/
|
||||||
|
public static LocalDateTime toLocalDateTime(String dateTimeString) {
|
||||||
|
|
||||||
|
Date date = toDate(dateTimeString);
|
||||||
|
if (date != null)
|
||||||
|
return toLocalDateTime(date);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if two time intervals overlaps.
|
||||||
|
* For this check, the library threeten-extra is used
|
||||||
|
*
|
||||||
|
* @param startA Interval A start
|
||||||
|
* @param endA Interval A end
|
||||||
|
* @param startB Interval B start
|
||||||
|
* @param endB Interval B End
|
||||||
|
* @return <code>true</code> if interval A overlaps interval B
|
||||||
|
*/
|
||||||
|
public static boolean overlaps(LocalDateTime startA, LocalDateTime endA, LocalDateTime startB, LocalDateTime endB) {
|
||||||
|
|
||||||
|
Interval intervalA = Interval.of(startA.atZone(ZoneId.systemDefault()).toInstant(), endA.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
Interval IntervalB = Interval.of(startB.atZone(ZoneId.systemDefault()).toInstant(), endB.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
|
|
||||||
|
return intervalA.overlaps(IntervalB);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateFormat getFormatter(String dateTimeString) {
|
||||||
|
|
||||||
|
if (IMPORT_DATEPATTERN.matcher(dateTimeString).matches())
|
||||||
|
return new SimpleDateFormat(IMPORT_DATEFORMAT);
|
||||||
|
else
|
||||||
|
return new SimpleDateFormat(DEFAULT_DATEFORMAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1 +1,11 @@
|
|||||||
spring.application.name=Garage appointment management
|
spring.application.name=Garage appointment management
|
||||||
|
|
||||||
|
# Datasource settings
|
||||||
|
spring.datasource.url=jdbc:h2:mem:etecture
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=password
|
||||||
|
spring.datasource.platform=h2
|
||||||
|
|
||||||
|
# init DB by script
|
||||||
|
spring.sql.init.mode=always
|
||||||
25
src/main/resources/data.sql
Normal file
25
src/main/resources/data.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- initial data
|
||||||
|
INSERT INTO MD_SERVICE (CODE, NAME, DURATION)
|
||||||
|
VALUES
|
||||||
|
('MOT', 'Motorinstandsetzung', 14400),
|
||||||
|
('OIL', 'Ölwechsel', 900),
|
||||||
|
('WHE', 'Radwechsel', 1800),
|
||||||
|
('FIX', 'Blechreparatur', 10800),
|
||||||
|
('INS', 'Hauptuntersuchung', 3600);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO GARAGE (CODE, NAME, MAX_APPOINTMENTS)
|
||||||
|
VALUES
|
||||||
|
('autohaus-schmidt', 'Autohaus Schmidt', 2),
|
||||||
|
('meisterbetrieb-bachstraße', 'Meisterbetrieb Bachstraße', 3);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO GARAGE_SERVICES (GARAGE_ID, SERVICE_ID, DURATION)
|
||||||
|
VALUES
|
||||||
|
(1, 1, 14400),
|
||||||
|
(1, 2, 900),
|
||||||
|
(1, 3, 1800),
|
||||||
|
(2, 2, 600),
|
||||||
|
(2, 4, 10800),
|
||||||
|
(2, 5, 3600);
|
||||||
|
|
||||||
47
src/main/resources/schema.sql
Normal file
47
src/main/resources/schema.sql
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
CREATE TABLE GARAGE (
|
||||||
|
ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
CODE VARCHAR(100) DEFAULT NULL UNIQUE,
|
||||||
|
NAME VARCHAR(200) DEFAULT NULL,
|
||||||
|
MAX_APPOINTMENTS INT NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
|
PRIMARY KEY (ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE MD_SERVICE (
|
||||||
|
ID BIGINT NOT NULL AUTO_INCREMENT UNIQUE,
|
||||||
|
CODE VARCHAR(5) NOT NULL UNIQUE,
|
||||||
|
NAME VARCHAR(50) DEFAULT NULL,
|
||||||
|
DURATION BIGINT DEFAULT 0,
|
||||||
|
|
||||||
|
PRIMARY KEY (ID)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE GARAGE_SERVICES (
|
||||||
|
GARAGE_ID BIGINT NOT NULL,
|
||||||
|
SERVICE_ID BIGINT NOT NULL,
|
||||||
|
DURATION BIGINT DEFAULT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT GARAGE_SERVICE_IDX UNIQUE (GARAGE_ID, SERVICE_ID),
|
||||||
|
|
||||||
|
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 TIMESTAMP 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)
|
||||||
|
);
|
||||||
38
src/test/java/de/etecture/ga/GarageAppointmentAppTests.java
Normal file
38
src/test/java/de/etecture/ga/GarageAppointmentAppTests.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package de.etecture.ga;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
import de.etecture.ga.model.Garage;
|
||||||
|
import de.etecture.ga.repository.GarageRepository;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class GarageAppointmentAppTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private GarageRepository garageRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testImportedGarageData() throws URISyntaxException {
|
||||||
|
|
||||||
|
Optional<Garage> testGarage = garageRepository.findByCode("test-data");
|
||||||
|
|
||||||
|
assertNotNull(testGarage.get(), "Garage should not be null");
|
||||||
|
assertEquals("Test Autohaus", testGarage.get().name());
|
||||||
|
assertTrue(testGarage.get().appointments().size() == 18, "Test Autohaus should have 19 appointments");
|
||||||
|
assertTrue(testGarage.get().garageServices().size() == 3, "Test Autohaus should have 3 services");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package de.etecture.ga;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
|
||||||
|
|
||||||
@SpringBootTest
|
|
||||||
class GarageAppointmentManagementApplicationTests {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void contextLoads() {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
160
src/test/java/de/etecture/ga/api/GarageApiControllerTest.java
Normal file
160
src/test/java/de/etecture/ga/api/GarageApiControllerTest.java
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
package de.etecture.ga.api;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import de.etecture.ga.repository.GarageRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integrationtest for the {@link WerkstattApi}
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
class GarageApiControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
|
@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 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 13:00"))
|
||||||
|
.andExpect(MockMvcResultMatchers.jsonPath("$.bis").value("02.01.2019 13:30"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetInvalidTermin() throws Exception {
|
||||||
|
|
||||||
|
this.mockMvc
|
||||||
|
.perform(
|
||||||
|
MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", testGarage.id(), "9999a"))
|
||||||
|
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isNotAcceptable());
|
||||||
|
|
||||||
|
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(18)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(8)));
|
||||||
|
|
||||||
|
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(2)));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetTerminvorschlaege() throws Exception {
|
||||||
|
|
||||||
|
this.mockMvc
|
||||||
|
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/terminvorschlag/", testGarage.id())
|
||||||
|
.param("leistungsId", "3").param("von", "05.01.2019 12:00").param("bis", "05.01.2019 13:00"))
|
||||||
|
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(3)));
|
||||||
|
|
||||||
|
this.mockMvc
|
||||||
|
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/terminvorschlag/", testGarage.id())
|
||||||
|
.param("leistungsId", "3").param("von", "05.01.2019 12:05").param("bis", "05.01.2019 13:00"))
|
||||||
|
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(2)));
|
||||||
|
|
||||||
|
this.mockMvc
|
||||||
|
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/terminvorschlag/", testGarage.id())
|
||||||
|
.param("leistungsId", "3").param("von", "08.01.2019 09:00").param("bis", "08.01.2019 10:00"))
|
||||||
|
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(1)));
|
||||||
|
|
||||||
|
this.mockMvc
|
||||||
|
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/terminvorschlag/", testGarage.id())
|
||||||
|
.param("leistungsId", "3").param("von", "08.01.2019 09:00").param("bis", "08.01.2019 13:30"))
|
||||||
|
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
|
||||||
|
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/test/resources/application.properties
Normal file
10
src/test/resources/application.properties
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
spring.application.name=Garage appointment management - test
|
||||||
|
|
||||||
|
# Datasource settings
|
||||||
|
spring.datasource.url=jdbc:h2:mem:etecture_test
|
||||||
|
spring.datasource.driverClassName=org.h2.Driver
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.password=password
|
||||||
|
|
||||||
|
# init DB by script
|
||||||
|
spring.sql.init.mode=always
|
||||||
12
src/test/resources/data.sql
Normal file
12
src/test/resources/data.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-- test data
|
||||||
|
INSERT INTO GARAGE (CODE, NAME, MAX_APPOINTMENTS)
|
||||||
|
VALUES
|
||||||
|
('test-data', 'Test Autohaus', 2);
|
||||||
|
|
||||||
|
|
||||||
|
INSERT INTO GARAGE_SERVICES (GARAGE_ID, SERVICE_ID, DURATION)
|
||||||
|
VALUES
|
||||||
|
(select id from GARAGE where CODE = 'test-data', 1, 14400),
|
||||||
|
(select id from GARAGE where CODE = 'test-data', 2, 900),
|
||||||
|
(select id from GARAGE where CODE = 'test-data', 3, 1800);
|
||||||
|
|
||||||
19
src/test/resources/import/test-data.csv
Normal file
19
src/test/resources/import/test-data.csv
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
APP_DATE,SERVICE
|
||||||
|
2019-01-02T13:00Z,WHE
|
||||||
|
2019-01-02T15:00Z,WHE
|
||||||
|
2019-01-04T08:15Z,MOT
|
||||||
|
2019-01-04T09:00Z,OIL
|
||||||
|
2019-01-05T10:15Z,MOT
|
||||||
|
2019-01-05T13:00Z,WHE
|
||||||
|
2019-01-07T08:15Z,OIL
|
||||||
|
2019-01-07T08:45Z,OIL
|
||||||
|
2019-01-08T09:15Z,MOT
|
||||||
|
2019-01-08T09:30Z,MOT
|
||||||
|
2019-01-08T08:00Z,WHE
|
||||||
|
2019-01-09T08:30Z,WHE
|
||||||
|
2019-01-09T10:45Z,OIL
|
||||||
|
2019-01-09T13:00Z,OIL
|
||||||
|
2019-01-10T15:15Z,WHE
|
||||||
|
2019-01-10T14:10Z,MOT
|
||||||
|
2019-01-11T12:15Z,OIL
|
||||||
|
2019-01-11T08:15Z,WHE
|
||||||
|
Reference in New Issue
Block a user