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 BikeExtension = TINK.Model.Bikes.Bike.BikeExtension; using System.Globalization; using TINK.Model.Station.Operator; using Xamarin.Forms; using System.Linq; using TINK.Model.MiniSurvey; using TINK.Services.CopriApi; using TINK.MultilingualResources; 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, new ResourceUrls(response.tariff_info_html, response.bike_info_html, response.agb_html, response.privacy_html, response.impress_html)); /// 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. /// Controls whether notify property changed events are fired or not. public static void Load( this IBikeInfoMutable bike, BikeInfoReservedOrBooked bikeInfo, string mailAddress, Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All) { 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 { /// Set default lock type to . public static LockModel DEFAULTLOCKMODEL = LockModel.Sigo; /// Creates a bike info object from copri response. /// Copri response for a disposable bike. public static BikeInfo Create(BikeInfoAvailable bikeInfo) { var lockModel = bikeInfo.GetLockModel(); if (lockModel.HasValue && lockModel.Value == LockModel.BordComputer) { // 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; } var lockType = lockModel.HasValue ? BikeExtension.GetLockType(lockModel.Value) : BikeExtension.GetLockType(DEFAULTLOCKMODEL); // Map bikes without "system"- entry in response to backend- locks. try { switch (lockType) { case LockType.Backend: return new Bike.CopriLock.BikeInfo( bikeInfo.bike, bikeInfo.station, new Bikes.Bike.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState()}.Build(), bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription) null), #endif bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description); case LockType.Bluethooth: return new Bike.BluetoothLock.BikeInfo( bikeInfo.bike, bikeInfo.GetBluetoothLockId(), bikeInfo.GetBluetoothLockGuid(), bikeInfo.station, bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription)null), #endif bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description); default: throw new ArgumentException($"Unsupported lock type {lockType} detected."); } } 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) { var lockModel = bikeInfo.GetLockModel(); if (lockModel.HasValue && lockModel.Value == LockModel.BordComputer) { // 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; } var lockType = lockModel.HasValue ? BikeExtension.GetLockType(lockModel.Value) : BikeExtension.GetLockType(DEFAULTLOCKMODEL); // Map bikes without "system"- entry in response to backend- locks. // Check if bike is a bluetooth lock bike. int lockSerial = bikeInfo.GetBluetoothLockId(); Guid lockGuid = bikeInfo.GetBluetoothLockGuid(); switch (bikeInfo.GetState()) { case InUseStateEnum.Reserved: try { switch (lockType) { case LockType.Bluethooth: return new Bike.BluetoothLock.BikeInfo( bikeInfo.bike, lockSerial, lockGuid, bikeInfo.GetUserKey(), bikeInfo.GetAdminKey(), bikeInfo.GetSeed(), bikeInfo.GetFrom(), mailAddress, bikeInfo.station, bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription)null), #endif dateTimeProvider, bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description); case LockType.Backend: return new Bike.CopriLock.BikeInfo( bikeInfo.bike, bikeInfo.GetFrom(), mailAddress, bikeInfo.station, new Bikes.Bike.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState() }.Build(), bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription)null), #endif dateTimeProvider, bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description); default: throw new ArgumentException($"Unsupported lock type {lockType} detected."); } } 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 { switch (lockModel) { case LockModel.ILockIt: return new Bike.BluetoothLock.BikeInfo( bikeInfo.bike, lockSerial, bikeInfo.GetBluetoothLockGuid(), bikeInfo.GetUserKey(), bikeInfo.GetAdminKey(), bikeInfo.GetSeed(), bikeInfo.GetFrom(), mailAddress, bikeInfo.station, bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription)null), #endif bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description); case LockModel.BordComputer: return new BikeInfo( bikeInfo.bike, LockModel.BordComputer, bikeInfo.GetIsDemo(), bikeInfo.GetGroup(), bikeInfo.GetWheelType(), bikeInfo.GetTypeOfBike(), bikeInfo.description, bikeInfo.station, bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : Create(bikeInfo.tariff_description), #else Create((TINK.Repository.Response.TariffDescription)null), #endif bikeInfo.GetFrom(), mailAddress, bikeInfo.timeCode); default: return new Bike.CopriLock.BikeInfo( bikeInfo.bike, bikeInfo.GetFrom(), mailAddress, bikeInfo.station, new Bikes.Bike.CopriLock.LockInfo.Builder { State = bikeInfo.GetCopriLockingState() }.Build(), bikeInfo.GetOperatorUri(), #if !NOTARIFFDESCRIPTION bikeInfo.rental_description != null ? Create(bikeInfo.rental_description) : 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; } } /// /// Creates rental description object from JSON- tarif description object. /// /// Source JSON object. /// Tariff description object. public static Bikes.Bike.RentalDescription Create(this TariffDescription tariffDesciption) { var bike = new Bikes.Bike.RentalDescription { Name = tariffDesciption?.name, #if USCSHARP9 Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null, #else Id = int.TryParse(tariffDesciption?.number, out int number) ? number : (int?)null, #endif }; if (!string.IsNullOrEmpty(tariffDesciption?.free_hours) && double.TryParse(tariffDesciption?.free_hours, NumberStyles.Any, CultureInfo.InvariantCulture, out double freeHours)) { // Free time. Unit hours,format floating point number. bike.TariffEntries.Add("1", new Bikes.Bike.RentalDescription.TariffElement { Description = AppResources.MessageBikesManagementTariffDescriptionFreeTimePerSession, Value =string.Format("{0} {1}", freeHours.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionHour) }); } if (!string.IsNullOrEmpty(tariffDesciption?.eur_per_hour) && double.TryParse(tariffDesciption?.eur_per_hour, NumberStyles.Any, CultureInfo.InvariantCulture, out double euroPerHour)) { // Euro per hour. Format floating point. bike.TariffEntries.Add("2", new Bikes.Bike.RentalDescription.TariffElement { Description = AppResources.MessageBikesManagementTariffDescriptionFeeEuroPerHour, Value = string.Format("{0} {1}", euroPerHour.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour) }); } if (!string.IsNullOrEmpty(tariffDesciption?.max_eur_per_day) && double.TryParse(tariffDesciption.max_eur_per_day, NumberStyles.Any, CultureInfo.InvariantCulture, out double maxEuroPerDay)) { // Max euro per day. Format floating point. bike.TariffEntries.Add("3", new Bikes.Bike.RentalDescription.TariffElement { Description = AppResources.MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay, Value = string.Format("{0} {1}", maxEuroPerDay.ToString("0.00"), AppResources.MessageBikesManagementMaxFeeEuroPerDay) }); } if (!string.IsNullOrEmpty(tariffDesciption?.abo_eur_per_month) && double.TryParse(tariffDesciption.abo_eur_per_month, NumberStyles.Any, CultureInfo.InvariantCulture, out double aboEuroPerMonth)) { // Abo per month bike.TariffEntries.Add("4", new Bikes.Bike.RentalDescription.TariffElement { Description = AppResources.MessageBikesManagementTariffDescriptionAboEuroPerMonth, Value = string.Format("{0} {1}", aboEuroPerMonth.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerMonth) }); } if (!string.IsNullOrEmpty(tariffDesciption?.operator_agb ?? string.Empty)) { bike.InfoEntries.Add("1", new Bikes.Bike.RentalDescription.InfoElement { Key = "AGB", Value = tariffDesciption.operator_agb }); } return bike; } /// /// Creates rental description object from JSON- tarif description object. /// /// Source JSON object. /// Tariff description object. public static Bikes.Bike.RentalDescription Create(this RentalDescription rentalDesciption) { Bikes.Bike.RentalDescription.TariffElement CreateTarifEntry(string[] elementValue) { return new Bikes.Bike.RentalDescription.TariffElement { Description = elementValue != null && elementValue.Length > 0 ? elementValue[0] : string.Empty, Value = elementValue != null && elementValue.Length > 1 ? elementValue[1] : string.Empty, }; } Bikes.Bike.RentalDescription.InfoElement CreateInfoElement(string[] elementValue) { return new Bikes.Bike.RentalDescription.InfoElement { Key = elementValue != null && elementValue.Length > 0 ? elementValue[0] : string.Empty, Value = elementValue != null && elementValue.Length > 1 ? elementValue[1] : string.Empty, }; } // Read tariff elements. var tarifEntries = rentalDesciption?.tarif_elements != null ? rentalDesciption.tarif_elements.Select(x => new { Key = x.Key, Value = CreateTarifEntry(x.Value) }).ToLookup(x => x.Key, x => x.Value).ToDictionary(x => x.Key, x => x.First()) : new Dictionary(); // Read info elements. var InfoEntries = rentalDesciption?.rental_info != null ? rentalDesciption.rental_info.Select(x => new { Key = x.Key, Value = CreateInfoElement(x.Value) }).ToLookup(x => x.Key, x => x.Value).ToDictionary(x => x.Key, x => x.First()) : new Dictionary(); var bike = new Bikes.Bike.RentalDescription { Name = rentalDesciption?.name ?? string.Empty, Id = int.TryParse(rentalDesciption?.id ?? string.Empty, out int number) ? number : (int?)null, TariffEntries = tarifEntries, InfoEntries = InfoEntries }; return bike; } /// 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; } } }