using System; using System.Threading.Tasks; using ShareeBike.Model.Connector; using ShareeBike.Model; using ShareeBike.MultilingualResources; using ShareeBike.Repository.Exception; using ShareeBike.View; using EndRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand; using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand; using Serilog; using ShareeBike.Services.BluetoothLock; namespace ShareeBike.ViewModel.Bikes.Bike { /// /// Return bike action. /// /// Type of owner. public class EndRentalActionViewModel : EndRentalCommand.IEndRentalCommandListener, DisconnectCommand.IDisconnectCommandListener { /// /// 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; } /// 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, Func viewUpdateManager, IViewService viewService, IBikesViewModel bikesViewModel) { SelectedBike = selectedBike; IsConnectedDelegate = isConnectedDelegate; ConnectorFactory = connectorFactory; 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 end rental progress. /// /// Current step to process. public void ReportStep(EndRentalCommand.Step step) { switch (step) { case EndRentalCommand.Step.GetLocation: // 1a.Step: Geolocation data BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepGPS; BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; break; case EndRentalCommand.Step.ReturnBike: // 1b.Step: Return bike BikesViewModel.ActionText = AppResources.ActivityTextReturningBike; break; } } /// /// Processes the get lock location state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(EndRentalCommand.State state, string details) { switch (state) { case EndRentalCommand.State.GeneralQueryLocationFailed: case EndRentalCommand.State.GPSNotSupportedException: case EndRentalCommand.State.NoGPSData: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorEndRentalUnknownLocation, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.GPSNotEnabledException: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorLockLocationOff, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.NoGPSPermissionsException: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorNoLocationPermission, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.ResponseException: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ActivityTextErrorInvalidResponseException, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.WebConnectFailed: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.NotAtStation: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, details, AppResources.MessageAnswerOk); break; case EndRentalCommand.State.GeneralEndRentalError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; await ViewService.DisplayAlert( AppResources.ErrorEndRentalTitle, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); break; } } /// /// Processes the disconnect lock progress. /// /// Current step to process. public void ReportStep(DisconnectCommand.Step step) { switch (step) { case DisconnectCommand.Step.DisconnectLock: BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; break; } } /// /// Processes the disconnect lock state. /// /// State to process. /// Textual details describing current state. public Task ReportStateAsync(DisconnectCommand.State state, string details) { switch (state) { case DisconnectCommand.State.GeneralDisconnectError: BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; break; } return Task.CompletedTask; } /// Return bike. public async Task EndRentalAsync() { Log.ForContext().Information("User request to end rental of bike {bikeId}.", SelectedBike.Id); // 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 }); BookingFinishedModel bookingFinished = null; try { #if USELOCALINSTANCE var command = new EndRentalCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else bookingFinished = await SelectedBike.ReturnBikeAsync(this); Log.ForContext().Information("Rental of bike {bikeId} ended successfully.", SelectedBike.Id); #endif } catch (Exception exception) { Log.ForContext().Information("Rental of bike {bikeId} could not be terminated. {@exception}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.IsIdle = true; return; } if (bookingFinished != null) { BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; // Disconnect lock. try { BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; #if USELOCALINSTANCE var command = new DisconnectCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else await SelectedBike.DisconnectAsync(this); Log.ForContext().Information("Lock of bike {bikeId} was disconnected successfully.", SelectedBike.Id); #endif } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} can not be disconnected. {@exception}", SelectedBike.Id, exception); } // 2.Step: User feedback on bike condition 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; } 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(); try { await ConnectorFactory(IsConnected).Command.DoSubmitFeedback( new UserFeedbackDto { BikeId = SelectedBike.Id, CurrentChargeBars = feedback.CurrentChargeBars, IsBikeBroken = feedback.IsBikeBroken, Message = feedback.Message }, feedBackUri); Log.ForContext().Information("Feedback for bike {bikeId} was submitted successfully.", SelectedBike.Id); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; Log.ForContext().Information("Feedback for bike {bikeId} can not be submitted.", SelectedBike.Id); if (exception is ResponseException copriException) { // Copri exception. Log.ForContext().Debug("COPRI returned an error. {response}", copriException.Response); } else { Log.ForContext().Debug("{@exception}", exception); } await ViewService.DisplayAlert( AppResources.ErrorSubmitFeedbackTitle, AppResources.ErrorSubmitFeedback, AppResources.MessageAnswerOk); } BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; // Confirmation message that rental is ended Log.ForContext().Information("Rental of bike {bikeId} was terminated successfully.", SelectedBike.Id); 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.ActionText = string.Empty; BikesViewModel.IsIdle = true; return; } else { BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return; } } } }