package de.rwth.idsg.bikeman.psinterface.rest; import com.google.common.base.Strings; import de.rwth.idsg.bikeman.domain.Booking; import de.rwth.idsg.bikeman.domain.CardAccount; import de.rwth.idsg.bikeman.domain.ErrorType; import de.rwth.idsg.bikeman.domain.OperationState; import de.rwth.idsg.bikeman.domain.Reservation; import de.rwth.idsg.bikeman.domain.ReservationState; import de.rwth.idsg.bikeman.domain.Transaction; import de.rwth.idsg.bikeman.ixsi.service.AvailabilityPushService; import de.rwth.idsg.bikeman.ixsi.service.ConsumptionPushService; import de.rwth.idsg.bikeman.ixsi.service.ExternalBookingPushService; import de.rwth.idsg.bikeman.ixsi.service.PlaceAvailabilityPushService; import de.rwth.idsg.bikeman.psinterface.dto.request.BootNotificationDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.ChargingStatusDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.CustomerAuthorizeDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.PedelecStatusDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.SlotDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.StartTransactionDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.StationStatusDTO; import de.rwth.idsg.bikeman.psinterface.dto.request.StopTransactionDTO; import de.rwth.idsg.bikeman.psinterface.dto.response.AuthorizeConfirmationDTO; import de.rwth.idsg.bikeman.psinterface.dto.response.BootConfirmationDTO; import de.rwth.idsg.bikeman.psinterface.dto.response.CardReadKeyDTO; import de.rwth.idsg.bikeman.psinterface.exception.PsException; import de.rwth.idsg.bikeman.psinterface.repository.PsiBookingRepository; import de.rwth.idsg.bikeman.psinterface.repository.PsiCustomerRepository; import de.rwth.idsg.bikeman.psinterface.repository.PsiPedelecRepository; import de.rwth.idsg.bikeman.psinterface.repository.PsiReservationRepository; import de.rwth.idsg.bikeman.psinterface.repository.PsiStationRepository; import de.rwth.idsg.bikeman.psinterface.repository.PsiTransactionRepository; import de.rwth.idsg.bikeman.service.ErrorHistoryService; import de.rwth.idsg.bikeman.service.OperationStateService; import de.rwth.idsg.bikeman.service.TransactionEventService; import de.rwth.idsg.bikeman.web.rest.exception.DatabaseException; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; import org.joda.time.LocalDateTime; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.inject.Inject; import java.util.List; import static de.rwth.idsg.bikeman.psinterface.exception.PsErrorCode.AUTH_ATTEMPTS_EXCEEDED; import static de.rwth.idsg.bikeman.psinterface.exception.PsErrorCode.CONSTRAINT_FAILED; /** * Created by swam on 04/08/14. */ @Service @Slf4j public class PsiService { @Inject private PsiCustomerRepository customerRepository; @Inject private PsiTransactionRepository transactionRepository; @Inject private PsiStationRepository stationRepository; @Inject private PsiBookingRepository bookingRepository; @Inject private PsiPedelecRepository pedelecRepository; @Inject private PsiReservationRepository reservationRepository; @Inject private ConsumptionPushService consumptionPushService; @Inject private AvailabilityPushService availabilityPushService; @Inject private PlaceAvailabilityPushService placeAvailabilityPushService; @Inject private ExternalBookingPushService externalBookingPushService; @Inject private TransactionEventService transactionEventService; @Inject private OperationStateService operationStateService; @Inject private ErrorHistoryService errorHistoryService; private static final Integer HEARTBEAT_INTERVAL_IN_SECONDS = 60; private static final int MAX_AUTH_RETRIES = 3; public BootConfirmationDTO handleBootNotification(BootNotificationDTO bootNotificationDTO) throws DatabaseException { stationRepository.updateAfterBoot(bootNotificationDTO); List<CardReadKeyDTO> cardKeys = stationRepository.getCardReadKeys(); BootConfirmationDTO bootConfirmationDTO = new BootConfirmationDTO(); bootConfirmationDTO.setTimestamp(DateTime.now()); bootConfirmationDTO.setHeartbeatInterval(HEARTBEAT_INTERVAL_IN_SECONDS); bootConfirmationDTO.setCardKeys(cardKeys); return bootConfirmationDTO; } @Transactional public AuthorizeConfirmationDTO handleAuthorize(CustomerAuthorizeDTO customerAuthorizeDTO) throws DatabaseException { log.info("Card with CardId {} start authorization from Station", customerAuthorizeDTO.getCardId()); CardAccount cardAccount = customerRepository.findByCardId(customerAuthorizeDTO.getCardId()); checkOperationState(cardAccount, customerAuthorizeDTO); checkPin(cardAccount, customerAuthorizeDTO); int actualRentedPedelecs = transactionRepository.countOpenTransactions(cardAccount.getCardId()); int canRentCount = cardAccount.getCurrentTariff().getTariff().getMaxNumberPedelecs(); return new AuthorizeConfirmationDTO(cardAccount.getCardId(), actualRentedPedelecs, canRentCount); } @Transactional public void handleStartTransaction(StartTransactionDTO startTransactionDTO) throws DatabaseException { transactionEventService.createAndSaveStartTransactionEvent(startTransactionDTO); Transaction transaction = transactionRepository.start(startTransactionDTO); List<Reservation> reservationList = reservationRepository.find(transaction.getCardAccount().getCardAccountId(), transaction.getPedelec().getPedelecId(), LocalDateTime.now()); Booking booking; if (reservationList == null || reservationList.isEmpty()) { log.debug("No reservation found for startTransaction."); booking = new Booking(); } else if (reservationList.size() == 1) { Reservation res = reservationList.get(0); reservationRepository.updateState(res, ReservationState.USED); booking = bookingRepository.findByReservation(res); log.debug("Found Reservation: {}", res); } else { throw new PsException("More than one reservation found", CONSTRAINT_FAILED); } booking.setTransaction(transaction); bookingRepository.save(booking); if (reservationList == null || reservationList.isEmpty()) { performExternalBookingPush(booking, transaction); log.debug("Perform External Booking for booking: {} and Transaction: {}", booking, transaction); } performStartPush(startTransactionDTO); log.debug("Perform Start Push for Transaction: {} and Booking: {}", transaction, booking); } @Transactional public void handleStopTransaction(StopTransactionDTO stopTransactionDTO) throws DatabaseException { transactionEventService.createAndSaveStopTransactionEvent(stopTransactionDTO); Transaction t = transactionRepository.stop(stopTransactionDTO); if (t != null) { performStopPush(stopTransactionDTO, t); } } public List<String> getAvailablePedelecs(String stationManufacturerId, String cardId) throws DatabaseException { if (Strings.isNullOrEmpty(cardId)) { log.debug("cardId is not set. Returning available pedelecs"); return pedelecRepository.findAvailablePedelecs(stationManufacturerId); } log.debug("Querying reserved pedelecs for cardId '{}'", cardId); List<String> pedelecs = pedelecRepository.findReservedPedelecs(stationManufacturerId, cardId); if (pedelecs.isEmpty()) { log.debug("cardId '{}' has no reservations. Returning available pedelecs", cardId); pedelecs = pedelecRepository.findAvailablePedelecs(stationManufacturerId); } return pedelecs; } public void handleStationStatusNotification(StationStatusDTO stationStatusDTO) { try { pushToIxsi(stationStatusDTO); } catch (Exception e) { log.warn("Error occurred during IXSI availability push", e); } stationRepository.updateStationStatus(stationStatusDTO); checkForStationErrors(stationStatusDTO); } public void handlePedelecStatusNotification(PedelecStatusDTO pedelecStatusDTO) { try { pushToIxsi(pedelecStatusDTO); } catch (Exception e) { log.warn("Error occurred during IXSI availability push", e); } pedelecRepository.updatePedelecStatus(pedelecStatusDTO); checkForPedelecErrors(pedelecStatusDTO); } public void handleChargingStatusNotification(List<ChargingStatusDTO> chargingStatusDTO) { pedelecRepository.updatePedelecChargingStatus(chargingStatusDTO); } // ------------------------------------------------------------------------- // Async calls // ------------------------------------------------------------------------- @Async private void performStopPush(StopTransactionDTO stopTransactionDTO, Transaction t) { Booking booking = bookingRepository.findByTransaction(t); consumptionPushService.report(booking); DateTime startDateTime = t.getStartDateTime().toDateTime(); availabilityPushService.arrivedAtPlace( stopTransactionDTO.getPedelecManufacturerId(), stopTransactionDTO.getStationManufacturerId(), startDateTime); placeAvailabilityPushService.reportChange(stopTransactionDTO.getStationManufacturerId()); } @Async private void performExternalBookingPush(Booking booking, Transaction transaction) { externalBookingPushService.report(booking, transaction); } @Async public void performStartPush(StartTransactionDTO startTransactionDTO) { availabilityPushService.takenFromPlace( startTransactionDTO.getPedelecManufacturerId(), startTransactionDTO.getTimestamp()); placeAvailabilityPushService.reportChange(startTransactionDTO.getStationManufacturerId()); } @Async private void checkForStationErrors(StationStatusDTO stationStatusDTO) { if (stationStatusDTO.getStationErrorCode() != null) { errorHistoryService.createAndSaveErrorHistoryEntry( ErrorType.STATION_ERROR, stationStatusDTO.getStationErrorCode(), stationStatusDTO.getStationErrorInfo(), stationStatusDTO.getStationManufacturerId() ); } for (SlotDTO.StationStatus slotDTO : stationStatusDTO.getSlots()) { checkForSlotErrors(slotDTO); } } @Async private void checkForPedelecErrors(PedelecStatusDTO pedelecStatusDTO) { if (pedelecStatusDTO.getPedelecErrorCode() != null) { errorHistoryService.createAndSaveErrorHistoryEntry( ErrorType.PEDELEC_ERROR, pedelecStatusDTO.getPedelecErrorCode(), pedelecStatusDTO.getPedelecErrorInfo(), pedelecStatusDTO.getPedelecManufacturerId() ); } } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- private void pushToIxsi(PedelecStatusDTO dto) { operationStateService.pushAvailability(dto); operationStateService.pushInavailability(dto); } private void pushToIxsi(StationStatusDTO dto) { operationStateService.pushAvailability(dto); operationStateService.pushInavailability(dto); } private void checkOperationState(CardAccount ca, CustomerAuthorizeDTO dto) { if (OperationState.OPERATIVE.equals(ca.getOperationState())) { return; } log.info("Card with CardId {} authorization failed with {}", dto.getCardId(), CONSTRAINT_FAILED); throw new PsException("Card account is disabled", CONSTRAINT_FAILED); } private void checkPin(CardAccount ca, CustomerAuthorizeDTO dto) { if (ca.getCardPin().equals(dto.getCardPin())) { // PIN is correct, reset auth trial count customerRepository.resetAuthenticationTrialCount(ca); return; } // increase auth fail count by one ca.setAuthenticationTrialCount(ca.getAuthenticationTrialCount() + 1); try { checkPinRetryLimit(ca); } finally { customerRepository.saveCardAccount(ca); } log.info("Card with CardId {} authorization failed (wrong pin) with {}", dto.getCardId(), CONSTRAINT_FAILED); throw new PsException("Wrong PIN", CONSTRAINT_FAILED); } private void checkPinRetryLimit(CardAccount ca) { boolean exceeded = ca.getAuthenticationTrialCount() >= MAX_AUTH_RETRIES; // auth attempts exceeded if (exceeded) { ca.setOperationState(OperationState.INOPERATIVE); log.warn("Card with CardId {} authorization failed (3x wrong pin) with {}", ca.getCardId(), AUTH_ATTEMPTS_EXCEEDED); throw new PsException("No trials remaining and account gets disabled", AUTH_ATTEMPTS_EXCEEDED); } } private void checkForSlotErrors(SlotDTO.StationStatus slotDTO) { if (slotDTO.getSlotErrorCode() != null) { errorHistoryService.createAndSaveErrorHistoryEntry( ErrorType.SLOT_ERROR, slotDTO.getSlotErrorCode(), slotDTO.getSlotErrorInfo(), slotDTO.getSlotManufacturerId() ); } } }