diff --git a/src/main/java/de/etecture/ga/GarageAppointmentManagementApplication.java b/src/main/java/de/etecture/ga/GarageAppointmentApp.java similarity index 62% rename from src/main/java/de/etecture/ga/GarageAppointmentManagementApplication.java rename to src/main/java/de/etecture/ga/GarageAppointmentApp.java index ad8cc1a..7a44b19 100644 --- a/src/main/java/de/etecture/ga/GarageAppointmentManagementApplication.java +++ b/src/main/java/de/etecture/ga/GarageAppointmentApp.java @@ -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); } } diff --git a/src/main/java/de/etecture/ga/api/GarageApiController.java b/src/main/java/de/etecture/ga/api/GarageApiController.java index 9eee602..db925dd 100644 --- a/src/main/java/de/etecture/ga/api/GarageApiController.java +++ b/src/main/java/de/etecture/ga/api/GarageApiController.java @@ -4,7 +4,6 @@ package de.etecture.ga.api; import java.time.LocalDateTime; -import java.util.Date; import java.util.List; import java.util.Optional; @@ -26,6 +25,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; /** + * Implementation of {@link WerkstattApi}. * */ @AllArgsConstructor @@ -35,38 +35,48 @@ public class GarageApiController implements WerkstattApi { private final AppointmentService appointmentService; + /** + * @see WerkstattApi#getTermin(String, String) + */ @Override public ResponseEntity 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 = 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> getTermine(@NotNull String werkstattId, @Valid String von, @Valid String bis, @Valid String leistungsId) { + // input validation Assert.isTrue(NumberUtils.isParsable(werkstattId), "werkstattId ungültig"); long garageId = Long.parseLong(werkstattId); Optional serviceId = NumberUtils.isParsable(leistungsId) ? Optional.of(Long.parseLong(leistungsId)) : Optional.empty(); - Optional appointmentsFrom = Optional.ofNullable(DateTimeUtil.toDate(von)); - Optional appointmentsTill = Optional.ofNullable(DateTimeUtil.toDate(bis)); - - log.info("Filter appointments by garage {}, serviceId {}, from {}, till {}", garageId, serviceId, - appointmentsFrom, appointmentsTill); + Optional appointmentsFrom = Optional.ofNullable(DateTimeUtil.toLocalDateTime(von)); + Optional 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> getTerminvorschlaege(String werkstattId, @NotNull @Valid String leistungsId, @Valid String von, @Valid String bis) { @@ -82,11 +92,15 @@ public class GarageApiController implements WerkstattApi { 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 postTermin(String werkstattId, @Valid TerminRequest termin) { diff --git a/src/main/java/de/etecture/ga/config/DataBaseConfiguration.java b/src/main/java/de/etecture/ga/config/DataBaseConfiguration.java index cb189da..204fd21 100644 --- a/src/main/java/de/etecture/ga/config/DataBaseConfiguration.java +++ b/src/main/java/de/etecture/ga/config/DataBaseConfiguration.java @@ -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 { diff --git a/src/main/java/de/etecture/ga/config/Setup.java b/src/main/java/de/etecture/ga/config/InitData.java similarity index 79% rename from src/main/java/de/etecture/ga/config/Setup.java rename to src/main/java/de/etecture/ga/config/InitData.java index 248cd8c..94958cf 100644 --- a/src/main/java/de/etecture/ga/config/Setup.java +++ b/src/main/java/de/etecture/ga/config/InitData.java @@ -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; diff --git a/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java index 7d44e04..bf3689f 100644 --- a/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java +++ b/src/main/java/de/etecture/ga/dto/mapper/AppointmentTerminMapper.java @@ -7,12 +7,15 @@ 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.info("Mapping Object {}", 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); diff --git a/src/main/java/de/etecture/ga/service/AppointmentService.java b/src/main/java/de/etecture/ga/service/AppointmentService.java index f6aa50c..83a0760 100644 --- a/src/main/java/de/etecture/ga/service/AppointmentService.java +++ b/src/main/java/de/etecture/ga/service/AppointmentService.java @@ -11,6 +11,7 @@ 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; @@ -21,6 +22,9 @@ 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 @@ -32,14 +36,40 @@ public class AppointmentService { 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 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); } - public List getAppointments(long garageId, Optional serviceId, Optional from, - Optional till) { + /** + * 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 getAppointments(long garageId, Optional serviceId, Optional from, + Optional till) { + + Assert.isTrue(garageId > 0, "garageId must be bigger than 0"); Stream appointments = repository.findByGarageId(garageId).stream(); @@ -47,19 +77,36 @@ public class AppointmentService { 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(from.get()) || a.appointmentTime().after(from.get())); + .filter(a -> a.appointmentTime().equals(fromDate) || a.appointmentTime().after(fromDate)); } if (till.isPresent()) { - appointments = appointments.filter(a -> a.appointmentTime().before(till.get())); + 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 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 = garageService.getGarage(garageId); if (garage.isEmpty()) throw new IllegalArgumentException("GarageId not valid"); @@ -75,10 +122,25 @@ public class AppointmentService { .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 createAppointment(long garageId, long serviceId, LocalDateTime from, LocalDateTime till) { - // validate input + 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 = garageService.getGarage(garageId); if (garage.isEmpty()) throw new IllegalArgumentException("GarageId not valid"); diff --git a/src/main/java/de/etecture/ga/service/GarageImportService.java b/src/main/java/de/etecture/ga/service/GarageImportService.java index bc33076..94b8d68 100644 --- a/src/main/java/de/etecture/ga/service/GarageImportService.java +++ b/src/main/java/de/etecture/ga/service/GarageImportService.java @@ -29,6 +29,9 @@ 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 @@ -65,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); } @@ -87,10 +90,10 @@ public class GarageImportService { private List loadObjectsFromFile(final Path file) { try { - CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader(); + CsvSchema schema = CsvSchema.emptySchema().withHeader(); CsvMapper mapper = new CsvMapper(); - MappingIterator readValues = mapper.readerFor(CSVData.class).with(bootstrapSchema) + MappingIterator readValues = mapper.readerFor(CSVData.class).with(schema) .readValues(file.toFile()); return readValues.readAll(); diff --git a/src/main/java/de/etecture/ga/service/GarageService.java b/src/main/java/de/etecture/ga/service/GarageService.java index 19ff05a..5a1f3bc 100644 --- a/src/main/java/de/etecture/ga/service/GarageService.java +++ b/src/main/java/de/etecture/ga/service/GarageService.java @@ -3,19 +3,31 @@ 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; - - - public Optional getGarage(long id) { - return repository.findById(id); + + /** + * 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 getGarage(long garageId) { + Assert.isTrue(garageId > 0, "A valid garageId must be given"); + + return repository.findById(garageId); } } diff --git a/src/main/java/de/etecture/ga/service/MDServiceService.java b/src/main/java/de/etecture/ga/service/MDServiceService.java index 9261f9f..d316b20 100644 --- a/src/main/java/de/etecture/ga/service/MDServiceService.java +++ b/src/main/java/de/etecture/ga/service/MDServiceService.java @@ -1,9 +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; @@ -11,35 +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 Optional getMDService(long id) { - return serviceRepository.findById(id); - } - - 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 getMDService(long serviceId) { + Assert.isTrue(serviceId > 0, "A valid serviceId must be given"); + + return serviceRepository.findById(serviceId); } } diff --git a/src/main/java/de/etecture/ga/util/DateTimeUtil.java b/src/main/java/de/etecture/ga/util/DateTimeUtil.java index 7c39df4..9f9b756 100644 --- a/src/main/java/de/etecture/ga/util/DateTimeUtil.java +++ b/src/main/java/de/etecture/ga/util/DateTimeUtil.java @@ -30,29 +30,61 @@ public class DateTimeUtil { 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, null 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, null if not + */ public static Date toDate(String dateTimeString) { if (StringUtils.isNotBlank(dateTimeString)) { @@ -67,6 +99,13 @@ public class DateTimeUtil { return null; } + /** + * Try to parse the given string into a {@link LocalDateTime}. + * If it was not possible to parse the string, null 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, null if not + */ public static LocalDateTime toLocalDateTime(String dateTimeString) { Date date = toDate(dateTimeString); @@ -76,7 +115,18 @@ public class DateTimeUtil { 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 true 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()); diff --git a/src/test/java/de/etecture/ga/GarageAppointmentManagementApplicationTests.java b/src/test/java/de/etecture/ga/GarageAppointmentAppTests.java similarity index 95% rename from src/test/java/de/etecture/ga/GarageAppointmentManagementApplicationTests.java rename to src/test/java/de/etecture/ga/GarageAppointmentAppTests.java index 760347f..19fc944 100644 --- a/src/test/java/de/etecture/ga/GarageAppointmentManagementApplicationTests.java +++ b/src/test/java/de/etecture/ga/GarageAppointmentAppTests.java @@ -15,7 +15,7 @@ import de.etecture.ga.model.Garage; import de.etecture.ga.repository.GarageRepository; @SpringBootTest -class GarageAppointmentManagementApplicationTests { +class GarageAppointmentAppTests { @Autowired private GarageRepository garageRepository;