sharee.bike-App/SharedBusinessLogic/Model/Bikes/BikeInfoNS/BikeNS/Command/EndRentalCommand.cs
2024-04-09 12:53:23 +02:00

258 lines
8 KiB
C#

using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Repository.Exception;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Repository.Request;
using System.Threading;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.MultilingualResources;
using Xamarin.Essentials;
namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command
{
public static class EndRentalCommand
{
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum Step
{
GetLocation,
ReturnBike,
}
/// <summary>
/// Possible steps of returning a bike.
/// </summary>
public enum State
{
GPSNotSupportedException,
GPSNotEnabledException,
NoGPSPermissionsException,
GeneralQueryLocationFailed,
WebConnectFailed,
NotAtStation,
NoGPSData,
ResponseException,
GeneralEndRentalError,
}
/// <summary>
/// Interface to notify view model about steps/ state changes of returning bike process.
/// </summary>
public interface IEndRentalCommandListener
{
/// <summary>
/// Reports current step.
/// </summary>
/// <param name="currentStep">Current step to report.</param>
void ReportStep(Step currentStep);
/// <summary>
/// Reports current state.
/// </summary>
/// <param name="currentState">Current state to report.</param>
/// <param name="message">Message describing the current state.</param>
/// <returns></returns>
Task ReportStateAsync(State currentState, string message);
}
/// <summary>
/// End rental.
/// </summary>
/// <param name="listener"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public static async Task<BookingFinishedModel> InvokeAsync<T>(
IBikeInfoMutable bike,
IGeolocationService geolocation,
ILocksService lockService,
Func<DateTime> dateTimeProvider = null,
Func<bool> isConnectedDelegate = null,
Func<bool, IConnector> connectorFactory = null,
IEndRentalCommandListener listener = null)
{
// Invokes member to notify about step being started.
void InvokeCurrentStep(Step step)
{
if (listener == null)
return;
try
{
listener.ReportStep(step);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step);
}
}
// Invokes member to notify about state change.
async Task InvokeCurrentStateAsync(State state, string message)
{
if (listener == null)
return;
try
{
await listener.ReportStateAsync(state, message);
}
catch (Exception exception)
{
Log.ForContext<T>().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state);
}
}
//// Start Action
// Get Location
//// Step: Start query geolocation data.
InvokeCurrentStep(Step.GetLocation);
// Get geolocation which was requested when closing lock.
var closingLockLocation = bike.LockInfo.Location;
LocationDto endRentalLocation;
if (closingLockLocation != null)
{
// Location was available when closing bike. No further actions required.
endRentalLocation =
new LocationDto.Builder
{
Latitude = closingLockLocation.Latitude,
Longitude = closingLockLocation.Longitude,
Accuracy = closingLockLocation.Accuracy ?? double.NaN,
Age = bike.LockInfo.LastLockingStateChange is DateTime lastLockState1 ? lastLockState1.Subtract(closingLockLocation.Timestamp.DateTime) : TimeSpan.MaxValue,
}.Build();
}
else
{
IGeolocation newLockLocation = null;
// Check if bike is around
var deviceState = lockService[bike.LockInfo.Id].GetDeviceState();
// Geolocation can not be queried because bike is not around.
if (deviceState != DeviceState.Connected)
{
Log.ForContext<T>().Information("There is no geolocation information available since lock of bike {bikeId} is not connected", bike.Id);
// no GPS data exception.
//NoGPSDataException.IsNoGPSData("There is no geolocation information available since lock is not connected", out NoGPSDataException exception);
await InvokeCurrentStateAsync(State.NoGPSData, "There is no geolocation information available since lock is not connected");
return null; // return empty BookingFinishedModel
}
else
{
// Bike is around -> Query geolocation.
var ctsLocation = new CancellationTokenSource();
try
{
newLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now);
}
catch (Exception exception)
{
// No location information available.
Log.ForContext<T>().Information("Geolocation query failed.");
if (exception is FeatureNotSupportedException)
{
Log.ForContext<T>().Error("Location service are not supported on device.");
await InvokeCurrentStateAsync(State.GPSNotSupportedException, exception.Message);
}
if (exception is FeatureNotEnabledException)
{
Log.ForContext<T>().Error("Location service are off.");
await InvokeCurrentStateAsync(State.GPSNotEnabledException, exception.Message);
}
if (exception is PermissionException)
{
Log.ForContext<T>().Error("No location service permissions granted.");
await InvokeCurrentStateAsync(State.NoGPSPermissionsException, exception.Message);
}
else
{
Log.ForContext<T>().Information("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralQueryLocationFailed, exception.Message);
}
throw;
}
}
// Update last lock state time
// save geolocation data for sending to backend
endRentalLocation = newLockLocation != null
? new LocationDto.Builder
{
Latitude = newLockLocation.Latitude,
Longitude = newLockLocation.Longitude,
Accuracy = newLockLocation.Accuracy ?? double.NaN,
Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(newLockLocation.Timestamp.DateTime),
}.Build()
: null;
}
// Return bike
InvokeCurrentStep(Step.ReturnBike);
BookingFinishedModel bookingFinished;
try
{
bookingFinished = await connectorFactory(true).Command.DoReturn(
bike,
endRentalLocation);
Log.ForContext<T>().Information("Rental of bike {bikeId} was terminated successfully.", bike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Rental of bike {bikeId} can not be terminated.", bike.Id);
if (exception is WebConnectFailureException)
{
// No web.
Log.ForContext<T>().Error("Copri server not reachable. No web.");
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
}
else if (exception is NotAtStationException notAtStationException)
{
// not at station.
Log.ForContext<T>().Error("COPRI returned out of GEO fencing error. Position send to COPRI {position}.", endRentalLocation);
await InvokeCurrentStateAsync(State.NotAtStation, string.Format(AppResources.ErrorEndRentalNotAtStation, notAtStationException.StationNr, notAtStationException.Distance));
// reset location -> at next try query new location data
bike.LockInfo.Location = null;
}
else if (exception is NoGPSDataException)
{
// no GPS data.
Log.ForContext<T>().Error("COPRI returned a no-GPS-data error.");
await InvokeCurrentStateAsync(State.NoGPSData, exception.Message);
// reset location -> at next try query new location data
bike.LockInfo.Location = null;
}
else if (exception is ResponseException copriException)
{
// COPRI exception.
Log.ForContext<T>().Error("COPRI returned an error. {response}", copriException.Response);
await InvokeCurrentStateAsync(State.ResponseException, exception.Message);
}
else
{
Log.ForContext<T>().Error("{@exception}", exception);
await InvokeCurrentStateAsync(State.GeneralEndRentalError, exception.Message);
}
throw;
}
return bookingFinished;
}
}
}