Implemented story 1 and missing utils
This commit is contained in:
8
pom.xml
8
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
return WerkstattApi.super.getTerminvorschlaege(werkstattId, leistungsId, von, bis);
|
.ok(appointmentService.getAppointmentSuggestion(garageId, serviceId, appointmentFrom, appointmentTill)
|
||||||
|
.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -42,20 +41,22 @@ public class Appointment implements Comparable<Appointment>{
|
|||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -14,9 +13,5 @@ public interface AppointmentRepository extends CrudRepository<Appointment, Long>
|
|||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/main/java/de/etecture/ga/util/DateTimeUtil.java
Normal file
94
src/main/java/de/etecture/ga/util/DateTimeUtil.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -27,10 +29,11 @@ class GarageAppointmentManagementApplicationTests {
|
|||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Reference in New Issue
Block a user