Compare commits

...

6 Commits

Author SHA1 Message Date
Matthias Engelien
0a7559b94f Better ilegalArgumentException handling 2024-09-16 08:09:50 +02:00
Matthias Engelien
ebc66ff8f6 Documentation 2024-09-16 07:59:27 +02:00
Matthias Engelien
86ecf3b317 Implemented story 1 and missing utils 2024-09-16 07:07:30 +02:00
Matthias Engelien
cbd6d373bb refactoring 2024-09-15 12:41:39 +02:00
Matthias Engelien
c311564ecc Creation of appointment implemented 2024-09-15 12:22:31 +02:00
Matthias Engelien
a2164a0eb3 Story 3 and Story 4 finished 2024-09-11 22:43:38 +02:00
19 changed files with 795 additions and 90 deletions

12
pom.xml
View File

@@ -36,6 +36,10 @@
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- OpenAPI generation -->
<dependency>
<groupId>org.springdoc</groupId>
@@ -53,13 +57,17 @@
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- confinience -->
<!-- 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 -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GarageAppointmentManagementApplication {
public class GarageAppointmentApp {
public static void main(String[] args) {
SpringApplication.run(GarageAppointmentManagementApplication.class, args);
SpringApplication.run(GarageAppointmentApp.class, args);
}
}

View File

@@ -3,44 +3,123 @@
*/
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(String werkstattId, String terminId) {
// TODO Auto-generated method stub
return WerkstattApi.super.getTermin(werkstattId, terminId);
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(String werkstattId, @Valid String von, @Valid String bis,
public ResponseEntity<List<Termin>> 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);
// 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) {
// TODO Auto-generated method stub
return WerkstattApi.super.getTerminvorschlaege(werkstattId, leistungsId, von, 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) {
// 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(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());
}
}

View File

@@ -11,6 +11,9 @@ 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 {

View File

@@ -7,10 +7,14 @@ 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 Setup {
public class InitData {
private final GarageImportService importService;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -1,32 +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)
public class Appointment {
@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());
}
}

View File

@@ -2,6 +2,7 @@ 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;
@@ -40,6 +41,10 @@ public class Garage {
}
return this;
}
public List<Appointment> appointmentsSorted() {
return this.appointments.stream().sorted().toList();
}
public Garage addService(MDService service) {
garageServices.add(createGarageService(service, null));

View File

@@ -1,9 +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);
}

View File

@@ -0,0 +1,218 @@
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");
Optional<Garage> garage = garageService.getGarage(garageId);
if (garage.isEmpty())
throw new IllegalArgumentException("GarageId not valid");
Optional<GarageServices> garageService = garage.get().garageServices().stream()
.filter(gs -> serviceId == gs.serviceId().getId()).findFirst();
if (garageService.isEmpty())
throw new IllegalArgumentException("serviceId not valid");
return getValidAppointmentTimeList(garage.get(), garageService.get(), 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");
Optional<Garage> garage = garageService.getGarage(garageId);
if (garage.isEmpty())
throw new IllegalArgumentException("GarageId not valid");
Optional<MDService> service = serviceService.getMDService(serviceId)
.filter(s -> garage.get().garageServices().stream().anyMatch(gs -> s.id() == gs.serviceId().getId()));
Optional<GarageServices> garageServices = garage.get().garageServices().stream()
.filter(gs -> serviceId == gs.serviceId().getId()).findFirst();
if (service.isEmpty() || garageServices.isEmpty())
throw new IllegalArgumentException("serviceId not valid");
// check appointment times
LocalDateTime validAppointmentTime = getValidAppointmentTime(garage.get(), garageServices.get(), 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(Optional<Garage> garage, Optional<MDService> service,
Optional<GarageServices> garageService, LocalDateTime validAppointmentTime) {
Appointment appointment = new Appointment().garageId(AggregateReference.to(garage.get().id()))
.appointmentTime(DateTimeUtil.toDate(validAppointmentTime))
.serviceId(AggregateReference.to(service.get().id())).serviceCode(service.get().code())
.serviceName(service.get().name()).duration(garageService.get().duration());
return appointment;
}
}

View File

@@ -25,9 +25,13 @@ 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
@@ -64,7 +68,7 @@ public class GarageImportService {
appointmentData.stream().filter(Objects::nonNull)
.forEach(data -> addAppointmentToGarage(data, garage.get()));
// bestehende Termine speichern
// save appointment data
garage.get().appointments().forEach(appointmentRepository::save);
}
@@ -86,9 +90,10 @@ public class GarageImportService {
private List<CSVData> loadObjectsFromFile(final Path file) {
try {
CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
CsvSchema schema = CsvSchema.emptySchema().withHeader();
CsvMapper mapper = new CsvMapper();
MappingIterator<CSVData> readValues = mapper.readerFor(CSVData.class).with(bootstrapSchema)
MappingIterator<CSVData> readValues = mapper.readerFor(CSVData.class).with(schema)
.readValues(file.toFile());
return readValues.readAll();
@@ -101,18 +106,27 @@ public class GarageImportService {
private void addAppointmentToGarage(CSVData data, Garage garage) {
Optional<MDService> garageService = garageServiceRepository.findByServiceCodeAndGarage(data.SERVICE,
garage.code());
Optional<MDService> garageService = garageServiceRepository
.findByServiceCodeAndGarage(data.appointmentServiceCode(), garage.code());
garageService.map(service -> getAppointmentForService(service, data.APP_DATE, garage.id()))
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));
.duration(service.duration()).garageId(AggregateReference.to(garageId))
.serviceId(AggregateReference.to(service.id()));
}
private record CSVData(Date APP_DATE, String SERVICE) {
private record CSVData(String APP_DATE, String SERVICE) {
public Date appointmentDate() {
return DateTimeUtil.toDate(APP_DATE);
}
public String appointmentServiceCode() {
return SERVICE;
}
}
}

View 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);
}
}

View File

@@ -1,8 +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;
import org.springframework.util.Assert;
@@ -10,30 +9,25 @@ 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;
public MDService storeMDService(String serviceCode) {
if (StringUtils.isBlank(serviceCode))
throw new InvalidParameterException("serviceCode should not been empty");
MDService service = serviceRepository.findByCode(serviceCode).orElse(new MDService().code(serviceCode));
return serviceRepository.save(service);
}
public MDService storeMDService(MDService serviceToSafe) {
Assert.notNull(serviceToSafe, "Service must not be null");
Assert.notNull(serviceToSafe.code(), "Service code must not be null");
Assert.isTrue(serviceToSafe.duration().isPositive(), "Service duration must must be bigger then 0");
MDService service = serviceRepository.findByCode(serviceToSafe.code()).orElse(serviceToSafe);
return serviceRepository.save(service);
/**
* 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);
}
}

View 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);
}
}

View File

@@ -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 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)
);

View File

@@ -1,6 +1,8 @@
package de.etecture.ga;
import static org.assertj.core.api.Assertions.assertThat;
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;
@@ -13,7 +15,7 @@ import de.etecture.ga.model.Garage;
import de.etecture.ga.repository.GarageRepository;
@SpringBootTest
class GarageAppointmentManagementApplicationTests {
class GarageAppointmentAppTests {
@Autowired
private GarageRepository garageRepository;
@@ -26,11 +28,12 @@ class GarageAppointmentManagementApplicationTests {
void testImportedGarageData() throws URISyntaxException {
Optional<Garage> testGarage = garageRepository.findByCode("test-data");
assertThat(testGarage).isPresent();
assertThat(testGarage.get().name()).isEqualTo("Test Autohaus");
assertThat(testGarage.get().appointments()).hasSize(19);
assertThat(testGarage.get().garageServices()).hasSize(3);
assertNotNull(testGarage.get(), "Garage should not be null");
assertEquals("Test Autohaus", testGarage.get().name());
assertTrue(testGarage.get().appointments().size() == 19, "Test Autohaus should have 19 appointments");
assertTrue(testGarage.get().garageServices().size() == 3, "Test Autohaus should have 3 services");
}
}

View File

@@ -1,18 +1,26 @@
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.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.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 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}
*/
@@ -22,32 +30,131 @@ 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 {
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());
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 testGetTermine() {
fail("Not yet implemented");
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 testGetTerminvorschlaege() {
fail("Not yet implemented");
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 testPostTermin() {
fail("Not yet implemented");
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"));
}
}

View File

@@ -3,7 +3,6 @@ APP_DATE,SERVICE
2019-01-02T15:00Z,WHE
2019-01-04T08:15Z,MOT
2019-01-04T09:00Z,OIL
2019-01-04T09:00Z,OIL
2019-01-05T10:15Z,MOT
2019-01-05T13:00Z,WHE
2019-01-07T08:15Z,OIL
1 APP_DATE SERVICE
3 2019-01-02T15:00Z WHE
4 2019-01-04T08:15Z MOT
5 2019-01-04T09:00Z OIL
2019-01-04T09:00Z OIL
6 2019-01-05T10:15Z MOT
7 2019-01-05T13:00Z WHE
8 2019-01-07T08:15Z OIL