package de.rwth.idsg.bikeman.ixsi.service; import de.rwth.idsg.bikeman.domain.Booking; import de.rwth.idsg.bikeman.domain.CardAccount; import de.rwth.idsg.bikeman.domain.OperationState; import de.rwth.idsg.bikeman.domain.Pedelec; import de.rwth.idsg.bikeman.domain.Reservation; import de.rwth.idsg.bikeman.domain.Transaction; import de.rwth.idsg.bikeman.ixsi.IxsiCodeException; import de.rwth.idsg.bikeman.ixsi.IxsiProcessingException; import de.rwth.idsg.bikeman.psinterface.dto.request.CancelReservationDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.ReserveNowDTO; import de.rwth.idsg.bikeman.repository.BookingRepository; import de.rwth.idsg.bikeman.repository.CardAccountRepository; import de.rwth.idsg.bikeman.repository.PedelecRepository; import de.rwth.idsg.bikeman.repository.ReservationRepository; import de.rwth.idsg.bikeman.service.StationService; import de.rwth.idsg.bikeman.web.rest.exception.DatabaseException; import lombok.extern.slf4j.Slf4j; import org.joda.time.Duration; import org.joda.time.LocalDateTime; import org.joda.time.Period; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import xjc.schema.ixsi.ErrorCodeType; import xjc.schema.ixsi.TimePeriodProposalType; import java.util.List; import java.util.concurrent.TimeUnit; /** * Created by max on 24/11/14. */ @Service @Slf4j public class BookingService { @Autowired private BookingRepository bookingRepository; @Autowired private ReservationRepository reservationRepository; @Autowired private CardAccountRepository cardAccountRepository; @Autowired private PedelecRepository pedelecRepository; @Autowired private StationService stationService; private static final int BOOKING_MIN_TIME_WINDOW_IN_MIN = 15; private static final int BOOKING_MAX_TIME_WINDOW_IN_MIN = 60; @Transactional public Booking createBookingForUser(String bookeeId, String cardId, TimePeriodProposalType timePeriodProposal) throws DatabaseException { LocalDateTime begin = new LocalDateTime(); LocalDateTime end = begin.plus(getBookingDuration(timePeriodProposal)); //checkTimeFrameForSanity(begin, end); if (timePeriodProposal.isSetMaxWait()) { // TODO Incorporate maxWait into processing. // Not sure about the approach: range search between begin and begin + maxWait for existing reservations? Period maxWait = timePeriodProposal.getMaxWait(); } Pedelec pedelec = pedelecRepository.findByManufacturerId(bookeeId); check(pedelec); CardAccount cardAccount = cardAccountRepository.findByCardId(cardId); check(cardAccount); // check for existing reservation in time frame List<Reservation> existingReservations = reservationRepository.findByTimeFrameForPedelec(pedelec.getPedelecId(), begin, end); if (existingReservations != null && !existingReservations.isEmpty()) { throw new IxsiProcessingException("There is an overlapping booking for the target in this time period"); } Reservation reservation = new Reservation(); reservation.setCardAccount(cardAccount); reservation.setStartDateTime(begin); reservation.setEndDateTime(end); reservation.setPedelec(pedelec); Reservation savedReservation = reservationRepository.save(reservation); // send reservation to station String endpointAddress = pedelec.getStationSlot().getStation().getEndpointAddress(); ReserveNowDTO reserveNowDTO = new ReserveNowDTO(pedelec.getManufacturerId(), cardAccount.getCardId(), end.toDateTime()); stationService.reserveNow(endpointAddress, reserveNowDTO); Booking booking = new Booking(); booking.setReservation(savedReservation); try { return bookingRepository.save(booking); } catch (Throwable e) { throw new DatabaseException("Failed during database operation.", e); } } public Booking get(String bookingId, String userId) { Booking b = bookingRepository.findByIxsiBookingIdForUser(bookingId, userId); checkState(b.getReservation().getCardAccount()); return b; } /** * @return Cancelled booking */ @Transactional public Booking cancel(String bookingId, String userId) { Booking booking = this.get(bookingId, userId); Transaction transaction = booking.getTransaction(); if (transaction != null) { throw new IxsiProcessingException("The pedelec is already taken, too late cannot cancel"); } Reservation reservation = booking.getReservation(); LocalDateTime end = reservation.getEndDateTime(); LocalDateTime now = new LocalDateTime(); if (now.isAfter(end)) { throw new IxsiProcessingException("The booking is already over, cannot cancel"); } bookingRepository.cancel(booking); // send 'cancelReservation' to station String endpointAddress = reservation.getPedelec().getStationSlot().getStation().getEndpointAddress(); CancelReservationDTO cancelReservationDTO = new CancelReservationDTO(reservation.getPedelec().getManufacturerId()); stationService.cancelReservation(endpointAddress, cancelReservationDTO); return booking; } @Transactional public Booking update(Booking booking, TimePeriodProposalType newTimePeriodProposal) { Reservation reservation = booking.getReservation(); LocalDateTime begin = new LocalDateTime(); LocalDateTime end = begin.plus(getBookingDuration(newTimePeriodProposal)); // check for new time period validity // TODO introduce max/min //checkTimeFrameForSanity(begin, end); List<Reservation> existingReservations = reservationRepository.findOverlappingReservations( reservation.getPedelec().getPedelecId(), reservation.getReservationId(), begin, end); if (!existingReservations.isEmpty()) { throw new IxsiProcessingException("Proposed time period overlaps existing booking."); } reservation.setStartDateTime(begin); reservation.setEndDateTime(end); reservationRepository.save(reservation); Pedelec pedelec = reservation.getPedelec(); CardAccount cardAccount = reservation.getCardAccount(); // send an updated reserve-now to station with new time String endpointAddress = pedelec.getStationSlot().getStation().getEndpointAddress(); ReserveNowDTO reserveNowDTO = new ReserveNowDTO(pedelec.getManufacturerId(), cardAccount.getCardId(), end.toDateTime()); stationService.reserveNow(endpointAddress, reserveNowDTO); return booking; } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /** * In this context, the actual IXSI begin and end timestamps are not important, * since BikeMan use case only defines to book pedelecs for a specified period of time * starting with the present time. * * Therefore we only extract the duration between them. */ private Duration getBookingDuration(TimePeriodProposalType tp) { Duration duration = new Duration(tp.getBegin(), tp.getEnd()); long millis = duration.getMillis(); boolean tooShort = millis < TimeUnit.MINUTES.toMillis(BOOKING_MIN_TIME_WINDOW_IN_MIN); if (tooShort) { throw new IxsiCodeException( "Desired booking time window is too short. Must be at least " + BOOKING_MIN_TIME_WINDOW_IN_MIN + " minutes", ErrorCodeType.BOOKING_TOO_SHORT); } boolean tooLong = millis > TimeUnit.MINUTES.toMillis(BOOKING_MAX_TIME_WINDOW_IN_MIN); if (tooLong) { throw new IxsiCodeException( "Desired booking time window is too long. Must be at most " + BOOKING_MAX_TIME_WINDOW_IN_MIN + " minutes", ErrorCodeType.BOOKING_TOO_LONG); } return duration; } /** * TODO: What is a reasonable value for lowerLimit? Is SoC check a good solution? */ private void check(Pedelec pedelec) { final double lowerLimit = 0.0; boolean isAvailable = OperationState.OPERATIVE.equals(pedelec.getState()) && !pedelec.getInTransaction() && pedelec.getChargingStatus().getBatteryStateOfCharge() > lowerLimit; if (!isAvailable) { throw new IxsiProcessingException("The booking target is not available."); } } private void check(CardAccount ca) { checkState(ca); if (ca.getInTransaction()) { throw new IxsiCodeException("The user is already in a transaction", ErrorCodeType.SYS_REQUEST_NOT_PLAUSIBLE); } } private void checkState(CardAccount ca) { if (ca.getOperationState() != OperationState.OPERATIVE) { throw new IxsiCodeException("The user cannot initiate any booking action", ErrorCodeType.SYS_REQUEST_NOT_PLAUSIBLE); } } /** * TODO: More rules/boundaries are needed for acceptable reservations * * 1) Min/max allowed duration? (reserve for 1 min / 5 years?) * 2) How soon is the begin? (start now / next year?) */ private void checkTimeFrameForSanity(LocalDateTime begin, LocalDateTime end) { LocalDateTime now = new LocalDateTime(); // Continue only if: now < start < end if (!(now.isBefore(begin) && begin.isBefore(end))) { throw new IxsiProcessingException("Unacceptable date/time values"); } } }