Implemented story 1 and missing utils

This commit is contained in:
Matthias Engelien
2024-09-16 07:07:30 +02:00
parent cbd6d373bb
commit 86ecf3b317
12 changed files with 263 additions and 126 deletions

View File

@@ -57,13 +57,17 @@
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- confinience --> <!-- convenient -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </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>

View File

@@ -3,13 +3,11 @@
*/ */
package de.etecture.ga.api; package de.etecture.ga.api;
import java.text.ParseException; import java.time.LocalDateTime;
import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -21,6 +19,7 @@ import de.etecture.ga.dto.TerminRequest;
import de.etecture.ga.dto.mapper.AppointmentTerminMapper; import de.etecture.ga.dto.mapper.AppointmentTerminMapper;
import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Appointment;
import de.etecture.ga.service.AppointmentService; import de.etecture.ga.service.AppointmentService;
import de.etecture.ga.util.DateTimeUtil;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -57,8 +56,8 @@ public class GarageApiController implements WerkstattApi {
long garageId = Long.parseLong(werkstattId); long garageId = Long.parseLong(werkstattId);
Optional<Long> serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId)) Optional<Long> serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId))
: Optional.empty(); : Optional.empty();
Optional<Date> appointmentsFrom = Optional.ofNullable(parseDate(von)); Optional<Date> appointmentsFrom = Optional.ofNullable(DateTimeUtil.toDate(von));
Optional<Date> appointmentsTill = Optional.ofNullable(parseDate(bis)); Optional<Date> appointmentsTill = Optional.ofNullable(DateTimeUtil.toDate(bis));
log.info("Filter appointments by garage {}, serviceId {}, from {}, till {}", garageId, serviceId, log.info("Filter appointments by garage {}, serviceId {}, from {}, till {}", garageId, serviceId,
appointmentsFrom, appointmentsTill); appointmentsFrom, appointmentsTill);
@@ -71,16 +70,21 @@ public class GarageApiController implements WerkstattApi {
@Override @Override
public ResponseEntity<List<Termin>> getTerminvorschlaege(String werkstattId, @NotNull @Valid String leistungsId, public ResponseEntity<List<Termin>> getTerminvorschlaege(String werkstattId, @NotNull @Valid String leistungsId,
@Valid String von, @Valid String bis) { @Valid String von, @Valid String bis) {
// input validation
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); 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 garageId = Long.parseLong(werkstattId);
Optional<Long> serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId)) long serviceId = Long.parseLong(leistungsId);
: Optional.empty(); LocalDateTime appointmentFrom = DateTimeUtil.toLocalDateTime(von);
Optional<Date> appointmentsFrom = Optional.ofNullable(parseDate(von)); LocalDateTime appointmentTill = DateTimeUtil.toLocalDateTime(bis);
Optional<Date> appointmentsTill = Optional.ofNullable(parseDate(bis));
return ResponseEntity
.ok(appointmentService.getAppointmentSuggestion(garageId, serviceId, appointmentFrom, appointmentTill)
return WerkstattApi.super.getTerminvorschlaege(werkstattId, leistungsId, von, bis); .stream().map(AppointmentTerminMapper::toTermin).toList());
} }
@Override @Override
@@ -89,34 +93,19 @@ public class GarageApiController implements WerkstattApi {
// input validation // input validation
Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig");
Assert.isTrue(NumberUtils.isParsable(termin.getLeistungsId()), "leistungsId ungültig"); Assert.isTrue(NumberUtils.isParsable(termin.getLeistungsId()), "leistungsId ungültig");
Assert.notNull(parseDate(termin.getVon()), "von ungültig"); Assert.notNull(DateTimeUtil.toDate(termin.getVon()), "von ungültig");
Assert.notNull(parseDate(termin.getBis()), "bis ungültig"); Assert.notNull(DateTimeUtil.toDate(termin.getBis()), "bis ungültig");
long garageId = Long.parseLong(werkstattId); long garageId = Long.parseLong(werkstattId);
long serviceId = Long.parseLong(termin.getLeistungsId()); long serviceId = Long.parseLong(termin.getLeistungsId());
Date appointmentFrom = parseDate(termin.getVon()); LocalDateTime appointmentFrom = DateTimeUtil.toLocalDateTime(termin.getVon());
Date appointmentTill = parseDate(termin.getBis()); LocalDateTime appointmentTill = DateTimeUtil.toLocalDateTime(termin.getBis());
// create appointment if possible // create appointment if possible
Optional<Appointment> appointment = appointmentService.createAppointment(garageId, serviceId, Optional<Appointment> appointment = appointmentService.createAppointment(garageId, serviceId, appointmentFrom,
appointmentFrom, appointmentTill); appointmentTill);
return appointment.map(a -> ResponseEntity.ok(AppointmentTerminMapper.toTermin(a))) return appointment.map(a -> ResponseEntity.ok(AppointmentTerminMapper.toTermin(a)))
.orElse(ResponseEntity.status(HttpStatus.CONFLICT).build()); .orElse(ResponseEntity.status(HttpStatus.CONFLICT).build());
} }
private Date parseDate(String dateTimeString) {
if (StringUtils.isNotBlank(dateTimeString)) {
try {
SimpleDateFormat format = new SimpleDateFormat("dd.MM.yyyy HH:mm");
return format.parse(dateTimeString);
} catch (ParseException e) {
log.error("Invalid data to parse", e);
}
}
return null;
}
} }

View File

@@ -1,9 +1,10 @@
package de.etecture.ga.dto.mapper; package de.etecture.ga.dto.mapper;
import java.time.format.DateTimeFormatter; import java.util.Optional;
import de.etecture.ga.dto.Termin; import de.etecture.ga.dto.Termin;
import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Appointment;
import de.etecture.ga.util.DateTimeUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@@ -13,13 +14,15 @@ public class AppointmentTerminMapper {
log.info("Mapping Object {}", appointment); log.info("Mapping Object {}", appointment);
return new Termin() String id = Optional.ofNullable(appointment.id()).map(i -> Long.toString(i)).orElse(null);
.id(Long.toString(appointment.id())) String serviceId = Optional.ofNullable(appointment.serviceId().getId()).map(i -> Long.toString(i)).orElse(null);
.leistungsId(Long.toString(appointment.serviceId().getId())) String serviceName = Optional.ofNullable(appointment.serviceName()).orElse(null);
.leistung(appointment.serviceName()) String garageName = "to_be_set";
.werkstattName("to_be_set") String from = Optional.ofNullable(appointment.appointmentStart()).map(DateTimeUtil::toString).orElse(null);
.von(appointment.appointmentStart().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))) String till = Optional.ofNullable(appointment.appointmentEnd()).map(DateTimeUtil::toString).orElse(null);
.bis(appointment.appointmentEnd().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")));
return new Termin().id(id).leistungsId(serviceId).leistung(serviceName).werkstattName(garageName).von(from)
.bis(till);
} }
} }

View File

@@ -1,15 +1,14 @@
package de.etecture.ga.model; package de.etecture.ga.model;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date; import java.util.Date;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import de.etecture.ga.util.DateTimeUtil;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@@ -17,46 +16,48 @@ import lombok.experimental.Accessors;
@Data @Data
@Accessors(fluent = true, chain = true) @Accessors(fluent = true, chain = true)
@EqualsAndHashCode(onlyExplicitlyIncluded = true) @EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Appointment implements Comparable<Appointment>{ public class Appointment implements Comparable<Appointment> {
@Id @Id
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private Long id; private Long id;
@Column("GARAGE_ID") @Column("GARAGE_ID")
private AggregateReference<Garage, Long> garageId; private AggregateReference<Garage, Long> garageId;
@Column("SERVICE_ID") @Column("SERVICE_ID")
private AggregateReference<MDService, Long> serviceId; private AggregateReference<MDService, Long> serviceId;
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private String serviceCode; private String serviceCode;
private String serviceName; private String serviceName;
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private Date appointmentTime; private Date appointmentTime;
@EqualsAndHashCode.Include @EqualsAndHashCode.Include
private Integer slot = 1; private Integer slot = 1;
private Duration duration = Duration.ZERO; private Duration duration = Duration.ZERO;
public LocalDateTime appointmentStart() { public LocalDateTime appointmentStart() {
return Instant.ofEpochMilli(this.appointmentTime().getTime()) if (this.appointmentTime == null)
.atZone(ZoneId.systemDefault()).toLocalDateTime(); return null;
return DateTimeUtil.toLocalDateTime(this.appointmentTime);
} }
public LocalDateTime appointmentEnd() { public LocalDateTime appointmentEnd() {
if (this.appointmentTime == null)
return null;
return this.appointmentStart().plus(this.duration()); return this.appointmentStart().plus(this.duration());
} }
@Override @Override
public int compareTo(Appointment o) { public int compareTo(Appointment o) {
if(o == null || o.appointmentTime() == null) return 1; if (o == null || o.appointmentTime() == null)
return 1;
return this.appointmentTime().compareTo(o.appointmentTime()); return this.appointmentTime().compareTo(o.appointmentTime());
} }
} }

View File

@@ -3,7 +3,6 @@ package de.etecture.ga.repository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.CrudRepository;
import de.etecture.ga.model.Appointment; import de.etecture.ga.model.Appointment;
@@ -11,12 +10,8 @@ import de.etecture.ga.model.Appointment;
public interface AppointmentRepository extends CrudRepository<Appointment, Long> { public interface AppointmentRepository extends CrudRepository<Appointment, Long> {
public Optional<Appointment> findByIdAndGarageId(long id, long garageId); public Optional<Appointment> findByIdAndGarageId(long id, long garageId);
public List<Appointment> findByGarageId(long garageId); public List<Appointment> findByGarageId(long garageId);
@Query("select a.* from APPOINTMENT a "
+ "join GARAGE g on g.ID = a.GARAGE_ID "
+ "join MD_SERVICE s on s.ID = a.SERVICE_ID "
+ "where s.CODE = :serviceCode and g.ID = :garageId ")
public List<Appointment> findByServiceCodeAndGarageId(String serviceCode, long garageId); public List<Appointment> findByServiceCodeAndGarageId(String serviceCode, long garageId);
} }

View File

@@ -1,9 +1,9 @@
package de.etecture.ga.service; package de.etecture.ga.service;
import java.time.Instant; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -17,10 +17,13 @@ import de.etecture.ga.model.Garage;
import de.etecture.ga.model.GarageServices; import de.etecture.ga.model.GarageServices;
import de.etecture.ga.model.MDService; import de.etecture.ga.model.MDService;
import de.etecture.ga.repository.AppointmentRepository; import de.etecture.ga.repository.AppointmentRepository;
import de.etecture.ga.util.DateTimeUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class AppointmentService { public class AppointmentService {
private final AppointmentRepository repository; private final AppointmentRepository repository;
@@ -29,12 +32,6 @@ public class AppointmentService {
private final MDServiceService serviceService; private final MDServiceService serviceService;
public Optional<Appointment> getAppointment(long appointmentId) {
return repository.findById(appointmentId);
}
public Optional<Appointment> getAppointment(long appointmentId, long garageId) { public Optional<Appointment> getAppointment(long appointmentId, long garageId) {
return repository.findByIdAndGarageId(appointmentId, garageId); return repository.findByIdAndGarageId(appointmentId, garageId);
@@ -60,7 +57,26 @@ public class AppointmentService {
return appointments.toList(); return appointments.toList();
} }
public Optional<Appointment> createAppointment(long garageId, long serviceId, Date from, Date till) { public List<Appointment> getAppointmentSuggestion(Long garageId, Long serviceId, LocalDateTime from,
LocalDateTime till) {
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();
}
public Optional<Appointment> createAppointment(long garageId, long serviceId, LocalDateTime from,
LocalDateTime till) {
// validate input // validate input
Optional<Garage> garage = garageService.getGarage(garageId); Optional<Garage> garage = garageService.getGarage(garageId);
@@ -69,17 +85,17 @@ public class AppointmentService {
Optional<MDService> service = serviceService.getMDService(serviceId) Optional<MDService> service = serviceService.getMDService(serviceId)
.filter(s -> garage.get().garageServices().stream().anyMatch(gs -> s.id() == gs.serviceId().getId())); .filter(s -> garage.get().garageServices().stream().anyMatch(gs -> s.id() == gs.serviceId().getId()));
Optional<GarageServices> garageService = garage.get().garageServices().stream() Optional<GarageServices> garageServices = garage.get().garageServices().stream()
.filter(gs -> serviceId == gs.serviceId().getId()).findFirst(); .filter(gs -> serviceId == gs.serviceId().getId()).findFirst();
if (service.isEmpty() || garageService.isEmpty()) if (service.isEmpty() || garageServices.isEmpty())
throw new IllegalArgumentException("serviceId not valid"); throw new IllegalArgumentException("serviceId not valid");
// check appointment times // check appointment times
LocalDateTime validAppointmentTime = getValidAppointmentTime(from, till, garage); LocalDateTime validAppointmentTime = getValidAppointmentTime(garage.get(), garageServices.get(), from, till);
// create appointment // create appointment
if (validAppointmentTime != null) { if (validAppointmentTime != null) {
Appointment appointment = createAppointmentObj(garage, service, garageService, validAppointmentTime); Appointment appointment = createAppointmentObj(garage, service, garageServices, validAppointmentTime);
return Optional.of(repository.save(appointment)); return Optional.of(repository.save(appointment));
@@ -88,24 +104,27 @@ public class AppointmentService {
} }
} }
private LocalDateTime getValidAppointmentTime(Date from, Date till, Optional<Garage> garage) { private List<LocalDateTime> getValidAppointmentTimeList(Garage garage, GarageServices service, LocalDateTime from,
LocalDateTime till) {
LocalDateTime fromDateTime = Instant.ofEpochMilli(from.getTime()).atZone(ZoneId.systemDefault()) List<LocalDateTime> result = new ArrayList<>();
.toLocalDateTime();
fromDateTime = roundUpToQuarter(fromDateTime);
LocalDateTime tillDateTime = Instant.ofEpochMilli(till.getTime()).atZone(ZoneId.systemDefault())
.toLocalDateTime();
LocalDateTime validAppointmentTime = null; from = roundUpToQuarter(from);
while (!fromDateTime.isAfter(tillDateTime)) {
if (isSlotAvailable(garage.get(), fromDateTime)) { while (from.isBefore(till)) {
validAppointmentTime = fromDateTime; if (isSlotAvailable(garage, from, service.duration())) {
break; result.add(from);
} }
fromDateTime = fromDateTime.plusMinutes(15); from = from.plusMinutes(15);
} }
return validAppointmentTime; 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) { private LocalDateTime roundUpToQuarter(LocalDateTime datetime) {
@@ -117,20 +136,19 @@ public class AppointmentService {
return datetime.plusMinutes(minutesToAdd).truncatedTo(ChronoUnit.MINUTES); return datetime.plusMinutes(minutesToAdd).truncatedTo(ChronoUnit.MINUTES);
} }
private boolean isSlotAvailable(Garage garage, LocalDateTime time) { private boolean isSlotAvailable(Garage garage, LocalDateTime startTime, Duration duration) {
long appointments = garage.appointments().stream() long appointments = garage.appointments().stream().filter(a -> DateTimeUtil.overlaps(a.appointmentStart(),
.filter(a -> (a.appointmentStart().isBefore(time) || a.appointmentStart().isEqual(time)) a.appointmentEnd(), startTime, startTime.plus(duration))).count();
&& a.appointmentEnd().isAfter(time))
.count();
return appointments < garage.maxAppointments(); return appointments < garage.maxAppointments();
} }
private Appointment createAppointmentObj(Optional<Garage> garage, Optional<MDService> service, private Appointment createAppointmentObj(Optional<Garage> garage, Optional<MDService> service,
Optional<GarageServices> garageService, LocalDateTime validAppointmentTime) { Optional<GarageServices> garageService, LocalDateTime validAppointmentTime) {
Appointment appointment = new Appointment().garageId(AggregateReference.to(garage.get().id())) Appointment appointment = new Appointment().garageId(AggregateReference.to(garage.get().id()))
.appointmentTime(Date.from(validAppointmentTime.atZone(ZoneId.systemDefault()).toInstant())) .appointmentTime(DateTimeUtil.toDate(validAppointmentTime))
.serviceId(AggregateReference.to(service.get().id())).serviceCode(service.get().code()) .serviceId(AggregateReference.to(service.get().id())).serviceCode(service.get().code())
.serviceName(service.get().name()).duration(garageService.get().duration()); .serviceName(service.get().name()).duration(garageService.get().duration());
return appointment; return appointment;

View File

@@ -15,7 +15,6 @@ import java.util.stream.Stream;
import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.MappingIterator; import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.dataformat.csv.CsvMapper; import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema; import com.fasterxml.jackson.dataformat.csv.CsvSchema;
@@ -26,6 +25,7 @@ import de.etecture.ga.model.MDService;
import de.etecture.ga.repository.AppointmentRepository; import de.etecture.ga.repository.AppointmentRepository;
import de.etecture.ga.repository.GarageRepository; import de.etecture.ga.repository.GarageRepository;
import de.etecture.ga.repository.GarageServiceRepository; import de.etecture.ga.repository.GarageServiceRepository;
import de.etecture.ga.util.DateTimeUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -89,7 +89,7 @@ public class GarageImportService {
try { try {
CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader(); CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader();
CsvMapper mapper = new CsvMapper(); CsvMapper mapper = new CsvMapper();
MappingIterator<CSVData> readValues = mapper.readerFor(CSVData.class).with(bootstrapSchema) MappingIterator<CSVData> readValues = mapper.readerFor(CSVData.class).with(bootstrapSchema)
.readValues(file.toFile()); .readValues(file.toFile());
@@ -103,18 +103,27 @@ public class GarageImportService {
private void addAppointmentToGarage(CSVData data, Garage garage) { private void addAppointmentToGarage(CSVData data, Garage garage) {
Optional<MDService> garageService = garageServiceRepository.findByServiceCodeAndGarage(data.SERVICE, Optional<MDService> garageService = garageServiceRepository
garage.code()); .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); .ifPresent(garage::addAppointment);
} }
private Appointment getAppointmentForService(MDService service, Date date, Long garageId) { private Appointment getAppointmentForService(MDService service, Date date, Long garageId) {
return new Appointment().appointmentTime(date).serviceCode(service.code()).serviceName(service.name()) return new Appointment().appointmentTime(date).serviceCode(service.code()).serviceName(service.name())
.duration(service.duration()).garageId(AggregateReference.to(garageId)).serviceId(AggregateReference.to(service.id())); .duration(service.duration()).garageId(AggregateReference.to(garageId))
.serviceId(AggregateReference.to(service.id()));
} }
private record CSVData(@JsonFormat(pattern="yyyy-MM-dd'T'HH:mm'Z'") 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,94 @@
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() {
}
public static Date toDate(LocalDateTime ldt) {
if (ldt == null)
return null;
return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
}
public static LocalDateTime toLocalDateTime(Date date) {
if (date == null)
return null;
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
public static String toString(Date date) {
return toString(toLocalDateTime(date));
}
public static String toString(LocalDateTime ldt) {
if (ldt == null)
return null;
return ldt.format(DateTimeFormatter.ofPattern(DEFAULT_DATEFORMAT));
}
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;
}
public static LocalDateTime toLocalDateTime(String dateTimeString) {
Date date = toDate(dateTimeString);
if (date != null)
return toLocalDateTime(date);
return null;
}
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

@@ -37,7 +37,7 @@ CREATE TABLE APPOINTMENT (
SERVICE_ID BIGINT NOT NULL, SERVICE_ID BIGINT NOT NULL,
SERVICE_CODE VARCHAR(5) DEFAULT NULL, SERVICE_CODE VARCHAR(5) DEFAULT NULL,
SERVICE_NAME VARCHAR(50) DEFAULT NULL, SERVICE_NAME VARCHAR(50) DEFAULT NULL,
APPOINTMENT_TIME DATE DEFAULT NULL, APPOINTMENT_TIME TIMESTAMP DEFAULT NULL,
SLOT INT NOT NULL DEFAULT 1, SLOT INT NOT NULL DEFAULT 1,
DURATION BIGINT NOT NULL DEFAULT 0, DURATION BIGINT NOT NULL DEFAULT 0,

View File

@@ -1,6 +1,8 @@
package de.etecture.ga; 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.net.URISyntaxException;
import java.util.Optional; import java.util.Optional;
@@ -26,11 +28,12 @@ class GarageAppointmentManagementApplicationTests {
void testImportedGarageData() throws URISyntaxException { void testImportedGarageData() throws URISyntaxException {
Optional<Garage> testGarage = garageRepository.findByCode("test-data"); Optional<Garage> testGarage = garageRepository.findByCode("test-data");
assertThat(testGarage).isPresent();
assertThat(testGarage.get().name()).isEqualTo("Test Autohaus"); assertNotNull(testGarage.get(), "Garage should not be null");
assertThat(testGarage.get().appointments()).hasSize(19); assertEquals("Test Autohaus", testGarage.get().name());
assertThat(testGarage.get().garageServices()).hasSize(3); 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,7 +1,6 @@
package de.etecture.ga.api; package de.etecture.ga.api;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -59,8 +58,8 @@ class GarageApiControllerTest {
.andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1")) .andExpect(MockMvcResultMatchers.jsonPath("$.id").value("1"))
.andExpect(MockMvcResultMatchers.jsonPath("$.leistung").value("Radwechsel")) .andExpect(MockMvcResultMatchers.jsonPath("$.leistung").value("Radwechsel"))
.andExpect(MockMvcResultMatchers.jsonPath("$.leistungsId").value("3")) .andExpect(MockMvcResultMatchers.jsonPath("$.leistungsId").value("3"))
.andExpect(MockMvcResultMatchers.jsonPath("$.von").value("02.01.2019 12:00")) .andExpect(MockMvcResultMatchers.jsonPath("$.von").value("02.01.2019 13:00"))
.andExpect(MockMvcResultMatchers.jsonPath("$.bis").value("02.01.2019 12:30")); .andExpect(MockMvcResultMatchers.jsonPath("$.bis").value("02.01.2019 13:30"));
} }
@@ -69,7 +68,7 @@ class GarageApiControllerTest {
this.mockMvc this.mockMvc
.perform( .perform(
MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", testGarage.id(), "20")) MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termin/{terminId}", testGarage.id(), "-5"))
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isNotFound()); .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isNotFound());
this.mockMvc this.mockMvc
@@ -86,7 +85,7 @@ class GarageApiControllerTest {
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()).param("thing", .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()).param("thing",
"somewhere")) "somewhere"))
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(19))); .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(18)));
} }
@Test @Test
@@ -96,7 +95,7 @@ class GarageApiControllerTest {
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id())
.param("leistungsId", "3")) .param("leistungsId", "3"))
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(7))); .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(8)));
this.mockMvc this.mockMvc
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) .perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id())
@@ -108,13 +107,36 @@ class GarageApiControllerTest {
.perform(MockMvcRequestBuilders.get("/werkstatt/{werkstattId}/termine", testGarage.id()) .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")) .param("leistungsId", "3").param("von", "05.01.2019 12:00").param("bis", "08.01.2019 12:00"))
.andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk()) .andDo(MockMvcResultHandlers.print()).andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(1))); .andExpect(MockMvcResultMatchers.jsonPath("$", hasSize(2)));
} }
@Test @Test
void testGetTerminvorschlaege() { void testGetTerminvorschlaege() throws Exception {
fail("Not yet implemented");
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 @Test

View File

@@ -3,7 +3,6 @@ APP_DATE,SERVICE
2019-01-02T15:00Z,WHE 2019-01-02T15:00Z,WHE
2019-01-04T08:15Z,MOT 2019-01-04T08:15Z,MOT
2019-01-04T09:00Z,OIL 2019-01-04T09:00Z,OIL
2019-01-04T09:00Z,OIL
2019-01-05T10:15Z,MOT 2019-01-05T10:15Z,MOT
2019-01-05T13:00Z,WHE 2019-01-05T13:00Z,WHE
2019-01-07T08:15Z,OIL 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