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; using TINK.Services.CopriApi; namespace TINK.Model.Connector { /// /// Connects TINK app to copri using JSON as input data format. /// /// Rename to UpdateFromCopri. public static class UpdaterJSON { /// Loads a bike object from copri server cancel reservation/ booking update request. /// Bike object to load response into. /// Controls whether notify property changed events are fired or not. public static void Load( this IBikeInfoMutable bike, Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel) { bike.State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel); } /// /// Gets all statsion for station provider and add them into station list. /// /// List of stations to update. 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( 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; } /// /// Gets general data from COPRI response. /// /// Response to get data from. /// General data object initialized form COPRI response. public static GeneralData GetGeneralData(this ResponseBase response) => new GeneralData( response.init_map.GetMapSpan(), response.merchant_message, response.TryGetCopriVersion(out Version copriVersion) ? new Version(0,0) : copriVersion); /// Gets account object from login response. /// Needed to extract cookie from autorization response. /// Response to get session cookie and debug level from. /// Mail address needed to construct a complete account object (is not part of response). /// Password needed to construct a complete account object (is not part of response). 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.GetIsAgbAcknowledged(), loginResponse.authcookie?.Replace(merchantId, ""), loginResponse.GetGroup(), loginResponse.debuglevel == 1 ? Permissions.All : (Permissions)loginResponse.debuglevel) ; } /// Load bike object from booking response. /// Bike object to load from response. /// Booking response. /// Mail address of user which books bike. /// Session cookie of user which books bike. /// Controls whether notify property changed events are fired or not. public static void Load( this IBikeInfoMutable bike, BikeInfoReservedOrBooked bikeInfo, string mailAddress, Func 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)); } } /// Gets bikes available from copri server response. /// Response to create collection from. /// New collection of available bikes. public static BikeCollection GetBikesAvailable( this BikesAvailableResponse bikesAvailableResponse) { return GetBikesAll( bikesAvailableResponse, new BikesReservedOccupiedResponse(), // There are no occupied bikes. string.Empty, () => DateTime.Now); } /// Gets bikes occupied from copri server response. /// Response to create bikes from. /// New collection of occupied bikes. public static BikeCollection GetBikesOccupied( this BikesReservedOccupiedResponse bikesOccupiedResponse, string mail, Func dateTimeProvider) { return GetBikesAll( new BikesAvailableResponse(), bikesOccupiedResponse, mail, dateTimeProvider); } /// Gets bikes occupied from copri server response. /// Response to create bikes from. /// New collection of occupied bikes. public static BikeCollection GetBikesAll( BikesAvailableResponse bikesAvailableResponse, BikesReservedOccupiedResponse bikesOccupiedResponse, string mail, Func dateTimeProvider) { var bikesDictionary = new Dictionary(); var duplicates = new Dictionary(); // 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); } } /// /// Constructs bike info instances/ bike info derived instances. /// 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; } } /// Creates a bike info object from copri response. /// Copri response. /// Mail address of user. /// Date and time provider function. /// public static BikeInfo Create( BikeInfoReservedOrBooked bikeInfo, string mailAddress, Func 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 }; } /// Creates a booking finished object from response. /// Response to create survey object from. public static BookingFinishedModel Create(this DoReturnResponse response) { var bookingFinished = new BookingFinishedModel { Co2Saving = response?.co2saving }; if (response?.user_miniquery == null) { return bookingFinished; } var miniquery = response.user_miniquery; bookingFinished.MiniSurvey = new MiniSurveyModel { Title = miniquery.title, Subtitle = miniquery.subtitle, Footer = miniquery.footer }; foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary().OrderBy(x => x.Key)) { if (string.IsNullOrEmpty(question.Key.Trim()) || question.Value.query == null) { // Skip invalid entries. continue; } bookingFinished.MiniSurvey.Questions.Add( question.Key, new MiniSurveyModel.QuestionModel()); } return bookingFinished; } } }