using System; using System.Threading.Tasks; using TINK.Model.Connector; using TINK.Model; using TINK.MultilingualResources; using TINK.Repository.Exception; using TINK.Repository.Request; using TINK.View; using static TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command.GetLockedLocationCommand; using Serilog; using TINK.Services.BluetoothLock; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock { /// /// Return bike action. /// /// Type of owner. public class EndRentalActionViewModel : IGetLockedLocationCommandListener { /// /// View model to be used for progress report and unlocking/ locking view. /// private IBikesViewModel BikesViewModel { get; set; } /// /// View service to show modal notifications. /// private IViewService ViewService { get; } /// Object to start or stop update of view model objects from Copri. private Func ViewUpdateManager { get; } /// Bike close. private Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable SelectedBike { get; } /// /// Service to control locks. /// private ILocksService LockService { get; } /// Provides a connector object. protected Func ConnectorFactory { get; } /// Delegate to retrieve connected state. private Func IsConnectedDelegate { get; } /// Gets the is connected state. bool IsConnected; /// /// Constructs the object. /// /// Bike to close. /// Object to start or stop update of view model objects from Copri. /// View service to show modal notifications. /// View model to be used for progress report and unlocking/ locking view. /// public EndRentalActionViewModel( Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, ILocksService lockService, Func viewUpdateManager, IViewService viewService, IBikesViewModel bikesViewModel) { SelectedBike = selectedBike; IsConnectedDelegate = isConnectedDelegate; ConnectorFactory = connectorFactory; LockService = lockService ?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel)}-object. Parameter {nameof(lockService)} must not be null."); ViewUpdateManager = viewUpdateManager; ViewService = viewService; BikesViewModel = bikesViewModel ?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel)}-object. {nameof(bikesViewModel)} must not be null."); // Set parameter for RentalProcess View to initial value. BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id) { State = CurrentRentalProcess.None, StepIndex = 0, Result = CurrentStepStatus.None }); } /// /// Processes the get lock location progress. /// /// Current step to process. public void ReportStep(Step step) { switch (step) { case Step.StartingQueryLocation: // 1.Step: Geolocation data BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepGPS; BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; break; case Step.DisconnectingLockOnDisconnectedNoLocationError: BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; break; } } /// /// Processes the get lock location state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(State state, string details) { switch (state) { case State.DisconnetedNoLocationError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorEndRentalNotAtSuitableStation, AppResources.MessageAnswerOk); break; case State.DisconnectError: BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; break; case State.QueryLocationSucceeded: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; break; case State.QueryLocationFailed: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorNoLocationPermission, AppResources.MessageAnswerOk); break; } } /// Return bike. public async Task EndRentalAsync() { Log.ForContext().Information("User requests to return bike {bike}.", SelectedBike); // lock GUI BikesViewModel.IsIdle = false; // Stop Updater BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopAsync(); // 1. Step // Parameter for RentalProcess View BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id) { State = CurrentRentalProcess.EndRental, StepIndex = 1, Result = CurrentStepStatus.None }); // Get Location BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepGPS; BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessEndRentalWait; LocationDto currentLocationDto = null; try { currentLocationDto = await SelectedBike.GetLockedBikeLocationAsync(this); } catch (Exception) { BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; return; } // Send end of rental to backend IsConnected = IsConnectedDelegate(); BookingFinishedModel bookingFinished; BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; try { bookingFinished = await ConnectorFactory(IsConnected).Command.DoReturn( SelectedBike, currentLocationDto); } catch (Exception exception) { BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected booked bike {bike} but returning failed (Copri server not reachable).", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else if (exception is NotAtStationException notAtStationException) { // COPRI returned an error. Log.ForContext().Information( "User selected booked bike {bike} but returning failed. COPRI returned out of GEO fencing error. Position send to COPRI {@position}.", SelectedBike, currentLocationDto); await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, string.Format(AppResources.ErrorEndRentalNotAtStation, notAtStationException.StationNr, notAtStationException.Distance), AppResources.MessageAnswerOk); } else if (exception is NoGPSDataException) { // COPRI returned an error. Log.ForContext().Information("User selected booked bike {bike} but returning failed. COPRI returned an no GPS- data error.", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, string.Format(AppResources.ErrorEndRentalUnknownLocation), AppResources.MessageAnswerOk); } else if (exception is ResponseException copriException) { // COPRI returned an error. Log.ForContext().Information("User selected booked bike {bike} but returning failed. COPRI returned an error.", SelectedBike); await ViewService.DisplayAdvancedAlert( AppResources.ErrorEndRentalTitle, copriException.Message, copriException.Response, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected booked bike {bike} but returning failed. {@l_oException}", SelectedBike.Id, exception); await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, exception.Message, AppResources.MessageAnswerOk); } await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; return; } BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; // 2.Step: User feedback on bike condition #if !USERFEEDBACKDLG_OFF BikesViewModel.RentalProcess.StepIndex = 2; BikesViewModel.RentalProcess.Result = CurrentStepStatus.None; BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepFeedback; BikesViewModel.RentalProcess.ImportantStepInfoText = String.Empty; var feedBackUri = SelectedBike?.OperatorUri; var battery = SelectedBike.Drive?.Battery; var feedback = await ViewService.DisplayUserFeedbackPopup( battery); if (battery != null && feedback.CurrentChargeBars != null) { SelectedBike.Drive.Battery.CurrentChargeBars = feedback.CurrentChargeBars; } #endif BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; // 3.Step // Send user feedback to backend BikesViewModel.RentalProcess.StepIndex = 3; BikesViewModel.RentalProcess.Result = CurrentStepStatus.None; BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepUpload; BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessEndRentalWait; IsConnected = IsConnectedDelegate(); #if !USERFEEDBACKDLG_OFF try { await ConnectorFactory(IsConnected).Command.DoSubmitFeedback( new UserFeedbackDto { BikeId = SelectedBike.Id, CurrentChargeBars = feedback.CurrentChargeBars, IsBikeBroken = feedback.IsBikeBroken, Message = feedback.Message }, feedBackUri); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Information("Submitting feedback for bike {bike} failed. COPRI returned an error.", SelectedBike); } else { Log.ForContext().Error("Submitting feedback for bike {bike} failed. {@l_oException}", SelectedBike.Id, exception); } await ViewService.DisplayAlert( AppResources.ErrorSubmitFeedbackTitle, AppResources.ErrorSubmitFeedback, AppResources.MessageAnswerOk); } #endif BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; // Disconnect lock. try { BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); } catch (Exception exception) { Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; // Confirmation message that rental is ended Log.ForContext().Information("User returned bike {bike} successfully.", SelectedBike); await ViewService.DisplayAlert( String.Format(AppResources.MessageRentalProcessEndRentalFinishedTitle, SelectedBike.Id), String.Format( "{0}{1}{2}{3}{4}", !string.IsNullOrWhiteSpace(bookingFinished?.Distance) ? $"{String.Format(AppResources.MessageRentalProcessEndRentalFinishedDistanceText, bookingFinished?.Distance)}\r\n" : string.Empty, !string.IsNullOrWhiteSpace(bookingFinished?.Co2Saving) ? $"{String.Format(AppResources.MessageRentalProcessEndRentalFinishedCO2SavingText, bookingFinished?.Co2Saving)}\r\n" : string.Empty, !string.IsNullOrWhiteSpace(bookingFinished?.Duration) ? $"{String.Format(AppResources.MessageRentalProcessEndRentalFinishedDurationText, bookingFinished?.Duration)}\r\n" : $"{string.Empty}", !string.IsNullOrWhiteSpace(bookingFinished?.RentalCosts) ? $"{String.Format(AppResources.MessageRentalProcessEndRentalFinishedRentalCostsText,bookingFinished?.RentalCosts)}\r\n" : $"{AppResources.MessageRentalProcessEndRentalFinishedNoRentalCostsText}\r\n", AppResources.MessageRentalProcessEndRentalFinishedText ), AppResources.MessageAnswerOk ); // Mini survey if (bookingFinished != null && bookingFinished.MiniSurvey.Questions.Count > 0) { await ViewService.PushModalAsync(ViewTypes.MiniSurvey); } BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.IsIdle = true; return; } } }