mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-12-23 15:46:29 +01:00
549 lines
24 KiB
C#
549 lines
24 KiB
C#
using System;
|
|
using TINK.Model.Bike;
|
|
using TINK.Model.Station;
|
|
using TINK.Repository.Response;
|
|
using TINK.Model.User.Account;
|
|
using System.Collections.Generic;
|
|
using TINK.Model.State;
|
|
using TINK.Repository.Exception;
|
|
using Serilog;
|
|
|
|
using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
|
|
using IBikeInfoMutable = TINK.Model.Bikes.Bike.BC.IBikeInfoMutable;
|
|
using System.Globalization;
|
|
using TINK.Model.Station.Operator;
|
|
using Xamarin.Forms;
|
|
using System.Linq;
|
|
using TINK.Model.MiniSurvey;
|
|
|
|
namespace TINK.Model.Connector
|
|
{
|
|
/// <summary>
|
|
/// Connects TINK app to copri using JSON as input data format.
|
|
/// </summary>
|
|
/// <todo>Rename to UpdateFromCopri.</todo>
|
|
public static class UpdaterJSON
|
|
{
|
|
/// <summary> Loads a bike object from copri server cancel reservation/ booking update request.</summary>
|
|
/// <param name="bike">Bike object to load response into.</param>
|
|
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
|
|
public static void Load(
|
|
this IBikeInfoMutable bike,
|
|
Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel)
|
|
{
|
|
|
|
bike.State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel);
|
|
}
|
|
/// <summary>
|
|
/// Gets all statsion for station provider and add them into station list.
|
|
/// </summary>
|
|
/// <param name="p_oStationList">List of stations to update.</param>
|
|
public static StationDictionary GetStationsAllMutable(this StationsAvailableResponse stationsAllResponse)
|
|
{
|
|
// Get stations from Copri/ file/ memory, ....
|
|
if (stationsAllResponse == null
|
|
|| stationsAllResponse.stations == null)
|
|
{
|
|
// Latest list of stations could not be retrieved from provider.
|
|
return new StationDictionary();
|
|
}
|
|
|
|
Version.TryParse(stationsAllResponse.copri_version, out Version copriVersion);
|
|
|
|
var stations = new StationDictionary(p_oVersion: copriVersion);
|
|
|
|
foreach (var station in stationsAllResponse.stations)
|
|
{
|
|
if (stations.GetById(station.Value.station) != null)
|
|
{
|
|
// Can not add station to list of station. Id is not unique.
|
|
throw new InvalidResponseException<StationsAvailableResponse>(
|
|
string.Format("Station id {0} is not unique.", station.Value.station), stationsAllResponse);
|
|
}
|
|
|
|
stations.Add(new Station.Station(
|
|
station.Value.station,
|
|
station.Value.GetGroup(),
|
|
station.Value.GetPosition(),
|
|
station.Value.description,
|
|
new Data(station.Value.operator_data?.operator_name,
|
|
station.Value.operator_data?.operator_phone,
|
|
station.Value.operator_data?.operator_hours,
|
|
station.Value.operator_data?.operator_email,
|
|
!string.IsNullOrEmpty(station.Value.operator_data?.operator_color)
|
|
? Color.FromHex(station.Value.operator_data?.operator_color)
|
|
: (Color?)null)));
|
|
}
|
|
|
|
return stations;
|
|
}
|
|
|
|
/// <summary> Gets account object from login response.</summary>
|
|
/// <param name="merchantId">Needed to extract cookie from autorization response.</param>
|
|
/// <param name="loginResponse">Response to get session cookie and debug level from.</param>
|
|
/// <param name="mail">Mail address needed to construct a complete account object (is not part of response).</param>
|
|
/// <param name="password">Password needed to construct a complete account object (is not part of response).</param>
|
|
public static IAccount GetAccount(
|
|
this AuthorizationResponse loginResponse,
|
|
string merchantId,
|
|
string mail,
|
|
string password)
|
|
{
|
|
if (loginResponse == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(loginResponse));
|
|
}
|
|
|
|
return new Account(
|
|
mail,
|
|
password,
|
|
loginResponse.authcookie?.Replace(merchantId, ""),
|
|
loginResponse.GetGroup(),
|
|
loginResponse.debuglevel == 1
|
|
? Permissions.All :
|
|
(Permissions)loginResponse.debuglevel) ;
|
|
}
|
|
|
|
/// <summary> Load bike object from booking response. </summary>
|
|
/// <param name="bike">Bike object to load from response.</param>
|
|
/// <param name="bikeInfo">Booking response.</param>
|
|
/// <param name="mailAddress">Mail address of user which books bike.</param>
|
|
/// <param name="p_strSessionCookie">Session cookie of user which books bike.</param>
|
|
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
|
|
public static void Load(
|
|
this IBikeInfoMutable bike,
|
|
BikeInfoReservedOrBooked bikeInfo,
|
|
string mailAddress,
|
|
Func<DateTime> dateTimeProvider,
|
|
Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All)
|
|
{
|
|
|
|
var l_oDateTimeProvider = dateTimeProvider != null
|
|
? dateTimeProvider
|
|
: () => DateTime.Now;
|
|
|
|
if (bike is Bike.BluetoothLock.BikeInfoMutable btBikeInfo)
|
|
{
|
|
btBikeInfo.LockInfo.Load(
|
|
bikeInfo.GetBluetoothLockId(),
|
|
bikeInfo.GetBluetoothLockGuid(),
|
|
bikeInfo.GetSeed(),
|
|
bikeInfo.GetUserKey(),
|
|
bikeInfo.GetAdminKey());
|
|
}
|
|
|
|
var l_oState = bikeInfo.GetState();
|
|
switch (l_oState)
|
|
{
|
|
case InUseStateEnum.Disposable:
|
|
bike.State.Load(
|
|
InUseStateEnum.Disposable,
|
|
notifyLevel: notifyLevel);
|
|
break;
|
|
|
|
case InUseStateEnum.Reserved:
|
|
bike.State.Load(
|
|
InUseStateEnum.Reserved,
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.timeCode,
|
|
notifyLevel);
|
|
break;
|
|
|
|
case InUseStateEnum.Booked:
|
|
bike.State.Load(
|
|
InUseStateEnum.Booked,
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.timeCode,
|
|
notifyLevel);
|
|
break;
|
|
|
|
default:
|
|
throw new Exception(string.Format("Unexpected bike state detected. state is {0}.", l_oState));
|
|
}
|
|
}
|
|
|
|
/// <summary> Gets bikes available from copri server response.</summary>
|
|
/// <param name="bikesAvailableResponse">Response to create collection from.</param>
|
|
/// <returns>New collection of available bikes.</returns>
|
|
public static BikeCollection GetBikesAvailable(
|
|
this BikesAvailableResponse bikesAvailableResponse)
|
|
{
|
|
return GetBikesAll(
|
|
bikesAvailableResponse,
|
|
new BikesReservedOccupiedResponse(), // There are no occupied bikes.
|
|
string.Empty,
|
|
() => DateTime.Now);
|
|
}
|
|
|
|
/// <summary> Gets bikes occupied from copri server response. </summary>
|
|
/// <param name="p_oBikesAvailable">Response to create bikes from.</param>
|
|
/// <returns>New collection of occupied bikes.</returns>
|
|
public static BikeCollection GetBikesOccupied(
|
|
this BikesReservedOccupiedResponse bikesOccupiedResponse,
|
|
string mail,
|
|
Func<DateTime> dateTimeProvider)
|
|
{
|
|
return GetBikesAll(
|
|
new BikesAvailableResponse(),
|
|
bikesOccupiedResponse,
|
|
mail,
|
|
dateTimeProvider);
|
|
}
|
|
|
|
/// <summary> Gets bikes occupied from copri server response. </summary>
|
|
/// <param name="p_oBikesAvailable">Response to create bikes from.</param>
|
|
/// <returns>New collection of occupied bikes.</returns>
|
|
public static BikeCollection GetBikesAll(
|
|
BikesAvailableResponse bikesAvailableResponse,
|
|
BikesReservedOccupiedResponse bikesOccupiedResponse,
|
|
string mail,
|
|
Func<DateTime> dateTimeProvider)
|
|
{
|
|
var bikesDictionary = new Dictionary<string, BikeInfo>();
|
|
var duplicates = new Dictionary<string, BikeInfo>();
|
|
|
|
// Get bikes from Copri/ file/ memory, ....
|
|
if (bikesAvailableResponse != null
|
|
&& bikesAvailableResponse.bikes != null)
|
|
{
|
|
foreach (var bikeInfoResponse in bikesAvailableResponse.bikes.Values)
|
|
{
|
|
var bikeInfo = BikeInfoFactory.Create(bikeInfoResponse);
|
|
if (bikeInfo == null)
|
|
{
|
|
// Response is not valid.
|
|
continue;
|
|
}
|
|
|
|
if (bikesDictionary.ContainsKey(bikeInfo.Id))
|
|
{
|
|
// Duplicates are not allowed.
|
|
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes available. Bike status is {bikeInfo.State.Value}.");
|
|
|
|
if (!duplicates.ContainsKey(bikeInfo.Id))
|
|
{
|
|
duplicates.Add(bikeInfo.Id, bikeInfo);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
|
|
}
|
|
}
|
|
|
|
// Get bikes from Copri/ file/ memory, ....
|
|
if (bikesOccupiedResponse != null
|
|
&& bikesOccupiedResponse.bikes_occupied != null)
|
|
{
|
|
foreach (var bikeInfoResponse in bikesOccupiedResponse.bikes_occupied.Values)
|
|
{
|
|
BikeInfo bikeInfo = BikeInfoFactory.Create(
|
|
bikeInfoResponse,
|
|
mail,
|
|
dateTimeProvider);
|
|
|
|
if (bikeInfo == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (bikesDictionary.ContainsKey(bikeInfo.Id))
|
|
{
|
|
// Duplicates are not allowed.
|
|
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes occupied. Bike status is {bikeInfo.State.Value}.");
|
|
if (!duplicates.ContainsKey(bikeInfo.Id))
|
|
{
|
|
duplicates.Add(bikeInfo.Id, bikeInfo);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
|
|
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
|
|
}
|
|
}
|
|
|
|
// Remove entries which are not unique.
|
|
foreach (var l_oDuplicate in duplicates)
|
|
{
|
|
bikesDictionary.Remove(l_oDuplicate.Key);
|
|
}
|
|
|
|
return new BikeCollection(bikesDictionary);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Constructs bike info instances/ bike info derived instances.
|
|
/// </summary>
|
|
public static class BikeInfoFactory
|
|
{
|
|
public static BikeInfo Create(BikeInfoAvailable bikeInfo)
|
|
{
|
|
if (bikeInfo.GetIsManualLockBike())
|
|
{
|
|
// Manual lock bikes are no more supported.
|
|
Log.Error(
|
|
$"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
|
|
"Manual lock bikes are no more supported." +
|
|
$"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $"station number {bikeInfo.station}" : string.Empty)}."
|
|
);
|
|
return null;
|
|
}
|
|
|
|
switch (bikeInfo.GetState())
|
|
{
|
|
case InUseStateEnum.Disposable:
|
|
break;
|
|
|
|
default:
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
|
|
return null;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(bikeInfo.station))
|
|
{
|
|
// Bike available must always have a station id because bikes can only be returned at a station.
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. No station info set.");
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
return !bikeInfo.GetIsBluetoothLockBike()
|
|
? new BikeInfo(
|
|
bikeInfo.bike,
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription) null),
|
|
#endif
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description)
|
|
: new Bike.BluetoothLock.BikeInfo(
|
|
bikeInfo.bike,
|
|
bikeInfo.GetBluetoothLockId(),
|
|
bikeInfo.GetBluetoothLockGuid(),
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription)null),
|
|
#endif
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description);
|
|
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
// Contructor reported invalid arguemts (missing lock id, ....).
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Invalid response detected. Available bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary> Creates a bike info object from copri response. </summary>
|
|
/// <param name="bikeInfo">Copri response. </param>
|
|
/// <param name="mailAddress">Mail address of user.</param>
|
|
/// <param name="dateTimeProvider">Date and time provider function.</param>
|
|
/// <returns></returns>
|
|
public static BikeInfo Create(
|
|
BikeInfoReservedOrBooked bikeInfo,
|
|
string mailAddress,
|
|
Func<DateTime> dateTimeProvider)
|
|
{
|
|
if (bikeInfo.GetIsManualLockBike())
|
|
{
|
|
// Manual lock bikes are no more supported.
|
|
Log.Error(
|
|
$"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " +
|
|
"Manual lock bikes are no more supported." +
|
|
$"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $", station number {bikeInfo.station}" : string.Empty)}."
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// Check if bike is a bluetooth lock bike.
|
|
var isBluetoothBike = bikeInfo.GetIsBluetoothLockBike();
|
|
int lockSerial = bikeInfo.GetBluetoothLockId();
|
|
Guid lockGuid = bikeInfo.GetBluetoothLockGuid();
|
|
|
|
switch (bikeInfo.GetState())
|
|
{
|
|
case InUseStateEnum.Reserved:
|
|
try
|
|
{
|
|
return !isBluetoothBike
|
|
? new BikeInfo(
|
|
bikeInfo.bike,
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description,
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription)null),
|
|
#endif
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.timeCode,
|
|
dateTimeProvider)
|
|
: new Bike.BluetoothLock.BikeInfo(
|
|
bikeInfo.bike,
|
|
lockSerial,
|
|
lockGuid,
|
|
bikeInfo.GetUserKey(),
|
|
bikeInfo.GetAdminKey(),
|
|
bikeInfo.GetSeed(),
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription)null),
|
|
#endif
|
|
dateTimeProvider,
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
// Contructor reported invalid arguemts (missing lock id, ....).
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Reserved bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
|
return null;
|
|
}
|
|
|
|
case InUseStateEnum.Booked:
|
|
try
|
|
{
|
|
return !isBluetoothBike
|
|
? new BikeInfo(
|
|
bikeInfo.bike,
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description,
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription)null),
|
|
#endif
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.timeCode)
|
|
: new Bike.BluetoothLock.BikeInfo(
|
|
bikeInfo.bike,
|
|
lockSerial,
|
|
bikeInfo.GetBluetoothLockGuid(),
|
|
bikeInfo.GetUserKey(),
|
|
bikeInfo.GetAdminKey(),
|
|
bikeInfo.GetSeed(),
|
|
bikeInfo.GetFrom(),
|
|
mailAddress,
|
|
bikeInfo.station,
|
|
bikeInfo.GetOperatorUri(),
|
|
#if !NOTARIFFDESCRIPTION
|
|
Create(bikeInfo.tariff_description),
|
|
#else
|
|
Create((TINK.Repository.Response.TariffDescription)null),
|
|
#endif
|
|
bikeInfo.GetIsDemo(),
|
|
bikeInfo.GetGroup(),
|
|
bikeInfo.GetWheelType(),
|
|
bikeInfo.GetTypeOfBike(),
|
|
bikeInfo.description);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
// Contructor reported invalid arguemts (missing lock id, ....).
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Booked bike with id {bikeInfo.bike} skipped. {ex.Message}");
|
|
return null;
|
|
}
|
|
|
|
default:
|
|
Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static Bikes.Bike.TariffDescription Create(this TariffDescription tariffDesciption)
|
|
{
|
|
return new Bikes.Bike.TariffDescription
|
|
{
|
|
Name = tariffDesciption?.name,
|
|
#if USCSHARP9
|
|
Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null,
|
|
#else
|
|
Number = int.TryParse(tariffDesciption?.number, out int number) ? number : (int?) null,
|
|
#endif
|
|
FreeTimePerSession = double.TryParse(tariffDesciption?.free_hours, NumberStyles.Any, CultureInfo.InvariantCulture, out double freeHours) ? TimeSpan.FromHours(freeHours) : TimeSpan.Zero,
|
|
FeeEuroPerHour = double.TryParse(tariffDesciption?.eur_per_hour, NumberStyles.Any, CultureInfo.InvariantCulture, out double euroPerHour) ? euroPerHour : double.NaN,
|
|
AboEuroPerMonth = double.TryParse(tariffDesciption?.abo_eur_per_month, NumberStyles.Any, CultureInfo.InvariantCulture, out double aboEuroPerMonth) ? aboEuroPerMonth : double.NaN,
|
|
MaxFeeEuroPerDay = double.TryParse(tariffDesciption?.max_eur_per_day, NumberStyles.Any, CultureInfo.InvariantCulture, out double maxEuroPerDay) ? maxEuroPerDay : double.NaN,
|
|
OperatorAgb = tariffDesciption?.operator_agb,
|
|
TrackingInfo = tariffDesciption?.track_info
|
|
};
|
|
}
|
|
|
|
/// <summary> Creates a survey object from response.</summary>
|
|
/// <param name="response">Response to create survey object from.</param>
|
|
public static MiniSurveyModel Create(this ReservationCancelReturnResponse response)
|
|
{
|
|
if (response?.user_miniquery == null)
|
|
{
|
|
return new MiniSurveyModel();
|
|
}
|
|
|
|
var miniquery = response.user_miniquery;
|
|
var survey = new MiniSurveyModel
|
|
{
|
|
Title = miniquery.title,
|
|
Subtitle = miniquery.subtitle,
|
|
Footer = miniquery.footer
|
|
};
|
|
|
|
foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary<string, MiniSurveyResponse.Question>().OrderBy(x => x.Key))
|
|
{
|
|
if (string.IsNullOrEmpty(question.Key.Trim())
|
|
|| question.Value.query == null)
|
|
{
|
|
// Skip invalid entries.
|
|
continue;
|
|
}
|
|
|
|
survey.Questions.Add(
|
|
question.Key,
|
|
new MiniSurveyModel.QuestionModel());
|
|
}
|
|
|
|
return survey;
|
|
}
|
|
}
|
|
}
|