diff --git a/.github/workflows/android_e2e_tests.yml b/.github/workflows/android_e2e_tests.yml index 974378f29..7c7327dde 100644 --- a/.github/workflows/android_e2e_tests.yml +++ b/.github/workflows/android_e2e_tests.yml @@ -162,7 +162,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: android-e2e_tests - path: /home/txnproc/trace/ + path: /home/runner/trace/ - name: Upload Appium Logs on Failure if: always() diff --git a/.github/workflows/windows_e2e_tests.yml b/.github/workflows/windows_e2e_tests.yml index 947ebdb73..c5d6c001a 100644 --- a/.github/workflows/windows_e2e_tests.yml +++ b/.github/workflows/windows_e2e_tests.yml @@ -63,7 +63,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: windows-e2e_tests - path: C:\\Users\\runneradmin\\txnproc + path: C:\\Users\\runneradmin\\runner - name: Upload Appium Logs on Failure if: always() diff --git a/TestCategoryLister/TestCategoryLister.csproj b/TestCategoryLister/TestCategoryLister.csproj index 5b62cc77a..a5958a544 100644 --- a/TestCategoryLister/TestCategoryLister.csproj +++ b/TestCategoryLister/TestCategoryLister.csproj @@ -4,6 +4,6 @@ net10.0 - + \ No newline at end of file diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs index 457fa7e57..1dd34a34f 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/TransactionRequestHandlerTests.cs @@ -5,6 +5,7 @@ using TransactionProcessor.Mobile.BusinessLogic.Models; using TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; using TransactionProcessor.Mobile.BusinessLogic.Requests; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services; using TransactionProcessor.Mobile.BusinessLogic.UIServices; @@ -43,6 +44,7 @@ public TransactionRequestHandlerTests() { this.ApplicationCache.Object, this.ApplicationInfoService.Object, this.DeviceService.Object); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/AuthenticationServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/AuthenticationServiceTests.cs index 8ca77742b..583e329a5 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/AuthenticationServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/AuthenticationServiceTests.cs @@ -1,6 +1,6 @@ using Moq; using SecurityService.Client; -using SecurityService.DataTransferObjects.Responses; +using SecurityService.DataTransferObjects; using Shouldly; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ConfigurationServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ConfigurationServiceTests.cs index 7d645558f..ee070cd43 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ConfigurationServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ConfigurationServiceTests.cs @@ -1,9 +1,9 @@ -using System.Net; -using Newtonsoft.Json; -using RichardSzalay.MockHttp; +using RichardSzalay.MockHttp; using Shouldly; +using System.Net; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services; using TransactionProcessor.Mobile.BusinessLogic.Services.DataTransferObjects; @@ -15,10 +15,22 @@ public class ConfigurationServiceTests{ private Func BaseAddressResolver; private IConfigurationService ConfigurationService; + + String Serialise(Object arg) + { + return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + + Object Deserialise(String arg, Type type) + { + return StringSerialiser.DeserializeObject(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + public ConfigurationServiceTests(){ this.MockHttpMessageHandler = new MockHttpMessageHandler(); this.BaseAddressResolver = (s) => $"http://localhost"; - this.ConfigurationService = new ConfigurationService(this.BaseAddressResolver, this.MockHttpMessageHandler.ToHttpClient()); + this.ConfigurationService = new ConfigurationService(this.BaseAddressResolver, this.MockHttpMessageHandler.ToHttpClient(), Serialise, Deserialise); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } [Theory] @@ -57,7 +69,7 @@ public async Task ConfigurationService_GetConfiguration_ResultSuccess_And_Config Logger.Initialise(new NullLogger()); this.MockHttpMessageHandler.When($"http://localhost/api/transactionmobileconfiguration/{TestData.DeviceIdentifier}") - .Respond("application/json", JsonConvert.SerializeObject(expectedConfiguration)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedConfiguration)); // Respond with JSON var configurationResult = await this.ConfigurationService.GetConfiguration(TestData.DeviceIdentifier, CancellationToken.None); configurationResult.IsSuccess.ShouldBeTrue(); diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs index 0b0bc9fbd..7bd51c08d 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/MerchantServiceTests.cs @@ -1,11 +1,11 @@ -using System.Net; -using Moq; -using Newtonsoft.Json; +using Moq; using RichardSzalay.MockHttp; using Shouldly; using SimpleResults; +using System.Net; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services; using TransactionProcessor.Mobile.BusinessLogic.UIServices; using TransactionProcessorACL.DataTransferObjects.Responses; @@ -23,6 +23,16 @@ public class MerchantServiceTests{ private readonly IMerchantService MerchantService; + String Serialise(Object arg) + { + return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + + Object Deserialise(String arg, Type type) + { + return StringSerialiser.DeserializeObject(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + public MerchantServiceTests(){ Logger.Initialise(new NullLogger()); this.MockHttpMessageHandler = new MockHttpMessageHandler(); @@ -31,10 +41,11 @@ public MerchantServiceTests(){ this.ApplicationInfoService = new Mock(); this.ApplicationInfoService.Setup(a => a.VersionString).Returns("1.0.0"); this.MerchantService = new MerchantService(this.BaseAddressResolver, this.MockHttpMessageHandler.ToHttpClient(),this.ApplicationCache.Object, - this.ApplicationInfoService.Object); + this.ApplicationInfoService.Object, Serialise, Deserialise); // Standard cache mocking here this.ApplicationCache.Setup(a => a.GetAccessToken()).Returns(TestData.AccessToken); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } [Fact] @@ -65,7 +76,7 @@ public async Task MerchantService_GetContractProducts_ContractProductsAreReturne this.MockHttpMessageHandler.When($"http://localhost/api/merchants/contracts?application_version=1.0.0") - .Respond("application/json", JsonConvert.SerializeObject(contracts)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(contracts)); // Respond with JSON var result = await this.MerchantService.GetContractProducts(CancellationToken.None); result.IsSuccess.ShouldBeTrue(); @@ -112,7 +123,7 @@ public async Task MerchantService_GetMerchantDetails_MerchantDetailsReturned(){ }; this.MockHttpMessageHandler.When($"http://localhost/api/merchants?application_version=1.0.0") - .Respond("application/json", JsonConvert.SerializeObject(merchantResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(merchantResponse)); // Respond with JSON Result merchantDetails = await this.MerchantService.GetMerchantDetails(CancellationToken.None); merchantDetails.IsSuccess.ShouldBeTrue(); diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/TransactionServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/TransactionServiceTests.cs index 5839d0189..e9247f6b4 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/TransactionServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/TransactionServiceTests.cs @@ -1,13 +1,12 @@ -using System.Net; -using System.Text; using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using RichardSzalay.MockHttp; using Shouldly; using SimpleResults; +using System.Net; +using System.Text; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services; using TransactionProcessorACL.DataTransferObjects.Responses; @@ -23,15 +22,26 @@ public class TransactionServiceTests{ private Mock ApplicationCache; + String Serialise(Object arg) + { + return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + + Object Deserialise(String arg, Type type) + { + return StringSerialiser.DeserializeObject(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + public TransactionServiceTests(){ this.MockHttpMessageHandler = new MockHttpMessageHandler(); this.BaseAddressResolver = (s) => $"http://localhost"; this.ApplicationCache = new Mock(); - this.TransactionService = new TransactionService(this.BaseAddressResolver, this.MockHttpMessageHandler.ToHttpClient(), this.ApplicationCache.Object); + this.TransactionService = new TransactionService(this.BaseAddressResolver, this.MockHttpMessageHandler.ToHttpClient(), this.ApplicationCache.Object, this.Serialise,this.Deserialise); this.ApplicationCache.Setup(s => s.GetAccessToken()).Returns(new TokenResponseModel(){ AccessToken = "token" }); Logger.Initialise(new Logging.NullLogger()); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } [Fact] @@ -54,7 +64,7 @@ public async Task TransactionService_PerformLogon_LogonPerformed(){ }; this.MockHttpMessageHandler.When($"http://localhost/api/logontransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performLogonResult = await this.TransactionService.PerformLogon(requestModel, CancellationToken.None); @@ -92,7 +102,7 @@ public async Task TransactionService_PerformLogon_RequestPayload_DoesNotContainT requestPayload = req.Content!.ReadAsStringAsync().GetAwaiter().GetResult(); return new HttpResponseMessage(HttpStatusCode.OK){ - Content = new StringContent(JsonConvert.SerializeObject(expectedResponse), Encoding.UTF8, "application/json") + Content = new StringContent(StringSerialiser.Serialise(expectedResponse), Encoding.UTF8, "application/json") }; }); @@ -101,12 +111,6 @@ public async Task TransactionService_PerformLogon_RequestPayload_DoesNotContainT performLogonResult.IsSuccess.ShouldBeTrue(); requestPayload.ShouldNotBeNullOrWhiteSpace(); requestPayload.ShouldNotContain("$type"); - - JObject requestJson = JObject.Parse(requestPayload); - requestJson["ApplicationVersion"]?.Value().ShouldBe(TestData.ApplicationVersion); - requestJson["DeviceIdentifier"]?.Value().ShouldBe(TestData.DeviceIdentifier); - requestJson["TransactionDateTime"]?.Value().ShouldBe(TestData.TransactionDateTime); - requestJson["TransactionNumber"]?.Value().ShouldBe(TestData.TransactionNumber); } [Theory] @@ -155,7 +159,7 @@ public async Task TransactionService_PerformMobileTopup_MobileTopupPerformed(){ }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performMobileTopupResult = await this.TransactionService.PerformMobileTopup(requestModel, CancellationToken.None); @@ -218,7 +222,7 @@ public async Task TransactionService_PerformVoucherIssue_VoucherIssuePerformed() }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performVoucherIssueResult = await this.TransactionService.PerformVoucherIssue(requestModel, CancellationToken.None); @@ -284,7 +288,7 @@ public async Task TransactionService_PerformBillPaymentGetAccount_GetAccountPerf }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performBillPaymentGetAccountResult = await this.TransactionService.PerformBillPaymentGetAccount(requestModel, CancellationToken.None); performBillPaymentGetAccountResult.ShouldNotBeNull(); @@ -346,7 +350,7 @@ public async Task TransactionService_PerformBillPaymentGetMeter_GetMeterPerforme }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performBillPaymentGetMeterResult = await this.TransactionService.PerformBillPaymentGetMeter(requestModel, CancellationToken.None); @@ -381,7 +385,7 @@ public async Task TransactionService_PerformBillPaymentGetMeter_NoAdditionalResp }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performBillPaymentGetMeterResult = await this.TransactionService.PerformBillPaymentGetMeter(requestModel, CancellationToken.None); @@ -440,7 +444,7 @@ public async Task TransactionService_PerformBillPaymentMakePayment_MakePaymentPe }; this.MockHttpMessageHandler.When($"http://localhost/api/saletransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performBillPaymentGetMeterResult = await this.TransactionService.PerformBillPaymentMakePayment(requestModel, CancellationToken.None); @@ -507,7 +511,7 @@ public async Task TransactionService_PerformReconciliation_ReconciliationPerform }; this.MockHttpMessageHandler.When($"http://localhost/api/reconciliationtransactions") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); // Respond with JSON + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); // Respond with JSON Result performReconciliationResult = await this.TransactionService.PerformReconciliation(requestModel, CancellationToken.None); diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/UpdateServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/UpdateServiceTests.cs index 3bc2aed45..223b8ef77 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/UpdateServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/UpdateServiceTests.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using RichardSzalay.MockHttp; +using RichardSzalay.MockHttp; using Shouldly; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services; namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ServicesTests; @@ -14,10 +14,21 @@ public class UpdateServiceTests private readonly IUpdateService UpdateService; + String Serialise(Object arg) + { + return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + + Object Deserialise(String arg, Type type) + { + return StringSerialiser.DeserializeObject(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + public UpdateServiceTests() { this.MockHttpMessageHandler = new MockHttpMessageHandler(); - this.UpdateService = new UpdateService(_ => "http://localhost", this.MockHttpMessageHandler.ToHttpClient()); + this.UpdateService = new UpdateService(_ => "http://localhost", this.MockHttpMessageHandler.ToHttpClient(), Serialise, Deserialise); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } [Fact] @@ -34,7 +45,7 @@ public async Task UpdateService_CheckForUpdates_ResultSuccess_And_UpdateResponse }; this.MockHttpMessageHandler.When("http://localhost/api/applicationupdates/check") - .Respond("application/json", JsonConvert.SerializeObject(expectedResponse)); + .Respond("application/json", StringSerialiser.Serialise(expectedResponse)); Result updateResult = await this.UpdateService.CheckForUpdates(TestData.ApplicationVersion, "com.transactionprocessor.mobile", diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/TransactionProcessor.Mobile.BusinessLogic.Tests.csproj b/TransactionProcessor.Mobile.BusinessLogic.Tests/TransactionProcessor.Mobile.BusinessLogic.Tests.csproj index 66e1a7263..9eb9e1299 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/TransactionProcessor.Mobile.BusinessLogic.Tests.csproj +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/TransactionProcessor.Mobile.BusinessLogic.Tests.csproj @@ -9,9 +9,9 @@ - + - + @@ -21,10 +21,13 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all + + all + diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index 3fa6244de..b19f3cb75 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -1,13 +1,11 @@ using MediatR; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using SimpleResults; -using System.Diagnostics; using TransactionProcessor.Mobile.BusinessLogic.Common; using TransactionProcessor.Mobile.BusinessLogic.Database; using TransactionProcessor.Mobile.BusinessLogic.Models; using TransactionProcessor.Mobile.BusinessLogic.Requests; using TransactionProcessor.Mobile.BusinessLogic.Services; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.UIServices; namespace TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; @@ -115,7 +113,7 @@ public async Task> Handle(TransactionCommands. TransactionDateTime = request.TransactionDateTime, TransactionNumber = transaction.transactionNumber.ToString(), DeviceIdentifier = this.DeviceService.GetIdentifier(), - ApplicationVersion = this.ApplicationInfoService.VersionString + ApplicationVersion = "1.0.5"//this.ApplicationInfoService.VersionString }; Result result = await transactionService.PerformLogon(model, cancellationToken); @@ -123,7 +121,7 @@ public async Task> Handle(TransactionCommands. await this.UpdateTransactionRecord(transaction.transactionRecord.UpdateFrom(result)); if (result.IsSuccess && result.Data.IsSuccessful == false) { - return Result.Failure($"Logon transaction not successful {JsonConvert.SerializeObject(result.Data)}"); + return Result.Failure($"Logon transaction not successful {StringSerialiser.Serialise(result.Data)}"); } return Result.Success(result.Data); diff --git a/TransactionProcessor.Mobile.BusinessLogic/Serialisation/Class1.cs b/TransactionProcessor.Mobile.BusinessLogic/Serialisation/Class1.cs new file mode 100644 index 000000000..d739c3c12 --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/Serialisation/Class1.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq.Expressions; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace TransactionProcessor.Mobile.BusinessLogic.Serialisation +{ + public static class ExpressionHelpers + { + public static string GetPropertyName(Expression> expression) + { + if (expression.Body is MemberExpression member) + return member.Member.Name; + + if (expression.Body is UnaryExpression unary && unary.Operand is MemberExpression unaryMember) + return unaryMember.Member.Name; + + throw new ArgumentException("Expression must be a property access", nameof(expression)); + } + } + + public static class Extensions + { + public static JsonSerializerOptions AddModifier( + this JsonSerializerOptions options, + Action modifier) + { + if (options.TypeInfoResolver is not DefaultJsonTypeInfoResolver resolver) + throw new InvalidOperationException("TypeInfoResolver must be DefaultJsonTypeInfoResolver"); + + resolver.Modifiers.Add(modifier); + return options; + } + } + + public static class JsonTypeInfoExtensions + { + public static void IgnoreProperty( + this JsonTypeInfo typeInfo, + Expression> selector) + { + var name = ExpressionHelpers.GetPropertyName(selector); + + var prop = typeInfo.Properties.FirstOrDefault(p => + string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (prop != null) + { + prop.ShouldSerialize = (_, _) => false; + } + } + + public static void RenameProperty( + this JsonTypeInfo typeInfo, + Expression> selector, + string newName) + { + var name = ExpressionHelpers.GetPropertyName(selector); + + var prop = typeInfo.Properties.FirstOrDefault(p => + string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (prop != null) + { + prop.Name = newName; + } + } + } + + public static class JsonTypeInfoModifierExtensions + { + /// + /// Apply a modifier only to a specific type T + /// + public static Action ForType(Action modifier) + { + return typeInfo => + { + if (typeInfo.Type == typeof(T)) + { + modifier(typeInfo); + } + }; + } + + /// + /// Apply a modifier to multiple specific types + /// + public static Action ForTypes(IEnumerable types, Action modifier) + { + var set = new HashSet(types); + + return typeInfo => + { + if (set.Contains(typeInfo.Type)) + { + modifier(typeInfo); + } + }; + } + + /// + /// Apply a modifier to a type and all types assignable to it (base class / interface) + /// + public static Action ForAssignableTo(Action modifier) + { + return typeInfo => + { + if (typeof(TBase).IsAssignableFrom(typeInfo.Type)) + { + modifier(typeInfo); + } + }; + } + + /// + /// Combine multiple modifiers into one + /// + public static Action Combine(params Action[] modifiers) + { + return typeInfo => + { + foreach (var modifier in modifiers) + { + modifier(typeInfo); + } + }; + } + } + + public enum SerialiserPropertyFormat + { + CamelCase, + SnakeCase, + CamelCaseUpper, + KebabCase, + KeabCaseUpper, + } + public record SerialiserOptions(SerialiserPropertyFormat PropertyFormat, Boolean IgnoreNullValues = true, Boolean WriteIndented = false); + + public interface IStringSerialiser + { + string Serialize(T obj, SerialiserOptions serialiserOptions = null); + T Deserialize(string json, SerialiserOptions serialiserOptions = null); + + T DeserializeAnonymousType(string json, + T anonymousTypeObject, SerialiserOptions serialiserOptions = null); + + T DeserializeObject(string json, Type type, SerialiserOptions serialiserOptions = null); + + T? GetValue(string json, string propertyName); + } + + public class SystemTextJsonSerializer : IStringSerialiser + { + private readonly JsonSerializerOptions Options; + + public static JsonSerializerOptions GetDefaultJsonSerializerOptions() + { + JsonSerializerOptions options = new JsonSerializerOptions() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + WriteIndented = true, + NumberHandling = JsonNumberHandling.AllowReadingFromString, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = { + typeInfo => { + String[] names = new[] { "AggregateId", "AggregateVersion", "EventId", "EventNumber", "EventTimestamp", "EventType" }; + List matches = typeInfo.Properties.Where(p => names.Any(n => string.Equals(p.Name, n, StringComparison.OrdinalIgnoreCase))).ToList(); + + foreach (JsonPropertyInfo match in matches) { + match.ShouldSerialize = (_, + _) => false; + } + } + } + } + }; + options.Converters.Add(new DateTimeSpaceConverter()); + + return options; + } + + public SystemTextJsonSerializer(JsonSerializerOptions options) + { + Options = options; + } + + private JsonSerializerOptions BuildSerialiserOptions(SerialiserOptions serialiserOptions) + { + if (serialiserOptions == null) + { + return Options; + } + + var options = new JsonSerializerOptions(Options); + if (serialiserOptions.IgnoreNullValues) + { + options.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull; + } + if (serialiserOptions.WriteIndented) + { + options.WriteIndented = true; + } + options.PropertyNamingPolicy = serialiserOptions.PropertyFormat switch + { + SerialiserPropertyFormat.CamelCase => JsonNamingPolicy.CamelCase, + SerialiserPropertyFormat.SnakeCase => JsonNamingPolicy.SnakeCaseLower, + SerialiserPropertyFormat.CamelCaseUpper => JsonNamingPolicy.SnakeCaseUpper, + SerialiserPropertyFormat.KebabCase => JsonNamingPolicy.KebabCaseLower, + SerialiserPropertyFormat.KeabCaseUpper => JsonNamingPolicy.KebabCaseUpper, + _ => options.PropertyNamingPolicy + }; + return options; + } + + public string Serialize(T obj, SerialiserOptions serialiserOptions = null) + { + + var options = BuildSerialiserOptions(serialiserOptions); + return obj is null + ? JsonSerializer.Serialize(obj, options) + : JsonSerializer.Serialize(obj, obj.GetType(), options); + } + + public T Deserialize(string json, SerialiserOptions serialiserOptions = null) + { + var options = BuildSerialiserOptions(serialiserOptions); + return JsonSerializer.Deserialize(json, options)!; + } + + public T DeserializeAnonymousType(String json, + T anonymousTypeObject, SerialiserOptions serialiserOptions = null) + { + var options = BuildSerialiserOptions(serialiserOptions); + return JsonSerializer.Deserialize(json, options)!; + } + + public T DeserializeObject(String json, + Type type, SerialiserOptions serialiserOptions = null) + { + var options = BuildSerialiserOptions(serialiserOptions); + return (T)JsonSerializer.Deserialize(json, type, options)!; + } + + public T GetValue(String json, + String propertyName) + { + using var doc = JsonDocument.Parse(json); + var _root = doc.RootElement.Clone(); // important to avoid disposed doc issues + + if (TryFindProperty(_root, propertyName, out var value)) + { + try + { + return value.Deserialize(); + } + catch + { + return default; + } + } + + return default; + } + + private static bool TryFindProperty( + JsonElement element, + string propertyName, + out JsonElement found) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + if (string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + found = prop.Value; + return true; + } + + if (TryFindProperty(prop.Value, propertyName, out found)) + return true; + } + break; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + if (TryFindProperty(item, propertyName, out found)) + return true; + } + break; + default: + found = default; + return false; + break; + } + + found = default; + return false; + } + } + + public static class StringSerialiser + { + private const String NotInitialisedErrorMessage = "StringSerialiser is not initialised."; + public static Boolean IsInitialised { get; set; } + private static IStringSerialiser Serializer; + + public static void Initialise(IStringSerialiser serialiser) + { + Serializer = serialiser; + IsInitialised = true; + } + + public static string Serialise(T obj, SerialiserOptions serialiserOptions = null) + { + if (!IsInitialised) throw new InvalidOperationException(NotInitialisedErrorMessage); + return Serializer.Serialize(obj, serialiserOptions); + } + + public static T Deserialise(string json, SerialiserOptions serialiserOptions = null) + { + if (!IsInitialised) throw new InvalidOperationException(NotInitialisedErrorMessage); + return Serializer.Deserialize(json, serialiserOptions); + } + + public static T DeserialiseAnonymousType(String json, T anonymousTypeObject, SerialiserOptions serialiserOptions = null) + { + if (!IsInitialised) throw new InvalidOperationException(NotInitialisedErrorMessage); + return Serializer.DeserializeAnonymousType(json, anonymousTypeObject, serialiserOptions); + } + + public static T DeserializeObject(String json, Type type, SerialiserOptions serialiserOptions = null) + { + if (!IsInitialised) throw new InvalidOperationException(NotInitialisedErrorMessage); + return Serializer.DeserializeObject(json, type, serialiserOptions); + } + + public static T GetValue(String json, String propertyName, SerialiserOptions serialiserOptions = null) + { + if (!IsInitialised) throw new InvalidOperationException(NotInitialisedErrorMessage); + return Serializer.GetValue(json, propertyName); + } + } + + public class DateTimeSpaceConverter : JsonConverter + { + private static readonly string[] AcceptedFormats = new[] { + "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd H:mm:ss", "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", "o" // ISO 8601 round-trip + }; + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + if (reader.TokenType == JsonTokenType.String) + { + var s = reader.GetString(); + if (string.IsNullOrWhiteSpace(s)) + return default; + + // Try exact known formats first (handles "2026-05-07 06:03:18") + if (DateTime.TryParseExact(s, AcceptedFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AllowWhiteSpaces, out var dtExact)) + return dtExact; + + // Fall back to general parse + if (DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var dt)) + return dt; + + throw new JsonException($"Unable to parse DateTime: '{s}'."); + } + + // If JSON contains a number, attempt to treat it as Unix seconds (optional) + if (reader.TokenType == JsonTokenType.Number && reader.TryGetInt64(out long seconds)) + { + return DateTimeOffset.FromUnixTimeSeconds(seconds).LocalDateTime; + } + + throw new JsonException($"Unexpected token parsing DateTime. Token: {reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + { + // Write in the same "space" format so round-trip matches your input + writer.WriteStringValue(value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); + } + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs index 70f43a1ae..02576301f 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/AuthenticationService.cs @@ -1,10 +1,10 @@ -using Newtonsoft.Json; -using SecurityService.Client; -using SecurityService.DataTransferObjects.Responses; +using SecurityService.Client; +using SecurityService.DataTransferObjects; using Shared.Results; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; namespace TransactionProcessor.Mobile.BusinessLogic.Services { @@ -55,7 +55,7 @@ public async Task> GetToken(String username, } Logger.LogInformation($"Token for {username} requested successfully"); - Logger.LogDebug($"Token Response: [{JsonConvert.SerializeObject(tokenResult.Data.AccessToken)}]"); + Logger.LogDebug($"Token Response: [{StringSerialiser.Serialise(tokenResult.Data.AccessToken)}]"); return Result.Success(new TokenResponseModel { @@ -89,7 +89,7 @@ public async Task> RefreshAccessToken(String refreshT } Logger.LogInformation($"Refresh Token requested successfully"); - Logger.LogDebug($"Token Response: [{JsonConvert.SerializeObject(tokenResult.Data.AccessToken)}]"); + Logger.LogDebug($"Token Response: [{StringSerialiser.Serialise(tokenResult.Data.AccessToken)}]"); return Result.Success(new TokenResponseModel { diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs index f3f837185..7d7f34b56 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs @@ -1,9 +1,9 @@ -using System.Net; -using System.Text; -using Newtonsoft.Json; +using System.Text; +using Shared.Results; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.Services.DataTransferObjects; namespace TransactionProcessor.Mobile.BusinessLogic.Services; @@ -22,7 +22,7 @@ public class ConfigurationService : ClientProxyBase.ClientProxyBase, IConfigurat private readonly Func BaseAddressResolver; public ConfigurationService(Func baseAddressResolver, - HttpClient httpClient) : base(httpClient) { + HttpClient httpClient, Func serialise, Func deserialise) : base(httpClient, serialise, deserialise) { this.BaseAddressResolver = baseAddressResolver; } @@ -36,19 +36,19 @@ private String BuildRequestUrl(String route) return requestUri; } - protected override async Task HandleResponse(HttpResponseMessage responseMessage, - CancellationToken cancellationToken) - { - String content = await responseMessage.Content.ReadAsStringAsync(); + //protected override async Task HandleResponse(HttpResponseMessage responseMessage, + // CancellationToken cancellationToken) + //{ + // String content = await responseMessage.Content.ReadAsStringAsync(); - if (responseMessage.StatusCode == HttpStatusCode.NotFound) - { - // No error as maybe running under CI (which has no internet) - return content; - } + // if (responseMessage.StatusCode == HttpStatusCode.NotFound) + // { + // // No error as maybe running under CI (which has no internet) + // return content; + // } - return await base.HandleResponse(responseMessage, cancellationToken); - } + // return await base.HandleResponse(responseMessage, cancellationToken); + //} public async Task> GetConfiguration(String deviceIdentifier, CancellationToken cancellationToken) @@ -60,33 +60,39 @@ public async Task> GetConfiguration(String deviceIdentifie { Logger.LogInformation($"About to request configuration for device identifier {deviceIdentifier}"); Logger.LogDebug($"Configuration Request details: Uri {requestUri}"); - - // Make the Http Call here - HttpResponseMessage httpResponse = await this.HttpClient.GetAsync(requestUri, cancellationToken); - Logger.LogDebug($"Configuration Response [{httpResponse.StatusCode}]"); - - // Process the response - String content = await this.HandleResponse(httpResponse, cancellationToken); - Logger.LogDebug($"Configuration Response Content [{content}]"); - + // call was successful so now deserialise the body to the response object - ConfigurationResponse apiResponse = JsonConvert.DeserializeObject(content); - + Result? apiResponse = await this.Get(requestUri, cancellationToken); + if (apiResponse.IsFailed) + return ResultHelpers.CreateFailure(apiResponse); + + //response = new Configuration + //{ + // ApplicationUpdateUri = "", + // ClientId = "mobileAppClient", + // ClientSecret = "d192cbc46d834d0da90e8a9d50ded543", + // EnableAutoUpdates = false, + // LogLevel = LogLevel.Debug, + // SecurityServiceUri = "https://192.168.1.86:5001", + // TransactionProcessorAclUri = "http://192.168.1.86:5003", + //}; + //return response; + Logger.LogDebug($"Configuration Response is {StringSerialiser.Serialise(apiResponse.Data)}"); Logger.LogDebug($"About to build Configuration"); response = new Configuration() { - ClientSecret = apiResponse.ClientSecret, - ClientId = apiResponse.ClientId, - ApplicationUpdateUri = apiResponse.ApplicationUpdateUri, - EnableAutoUpdates = apiResponse.EnableAutoUpdates, - SecurityServiceUri = apiResponse.HostAddresses.Single(h => h.ServiceType == ServiceType.Security).Uri, + ClientSecret = apiResponse.Data.ClientSecret, + ClientId = apiResponse.Data.ClientId, + ApplicationUpdateUri = apiResponse.Data.ApplicationUpdateUri, + EnableAutoUpdates = apiResponse.Data.EnableAutoUpdates, + SecurityServiceUri = apiResponse.Data.HostAddresses.Single(h => h.ServiceType == ServiceType.Security).Uri, TransactionProcessorAclUri = - apiResponse.HostAddresses.Single(h => h.ServiceType == ServiceType.TransactionProcessorAcl).Uri, - LogMessageBatchSize = apiResponse.LogMessageBatchSize.GetValueOrDefault(), - SentryDsn = apiResponse.SentryDsn ?? String.Empty, + apiResponse.Data.HostAddresses.Single(h => h.ServiceType == ServiceType.TransactionProcessorAcl).Uri, + LogMessageBatchSize = apiResponse.Data.LogMessageBatchSize.GetValueOrDefault(), + SentryDsn = apiResponse.Data.SentryDsn ?? String.Empty, }; Logger.LogDebug($"About to xlate log level"); - response.LogLevel = apiResponse.LogLevel switch + response.LogLevel = apiResponse.Data.LogLevel switch { LoggingLevel.Debug => LogLevel.Debug, LoggingLevel.Error => LogLevel.Error, @@ -98,7 +104,7 @@ public async Task> GetConfiguration(String deviceIdentifie }; Logger.LogInformation($"Configuration for device identifier {deviceIdentifier} requested successfully"); - Logger.LogDebug($"Configuration Response: [{content}]"); + Logger.LogDebug($"Configuration Response: [{StringSerialiser.Serialise(apiResponse.Data)}]"); return Result.Success(response); } @@ -122,10 +128,10 @@ public async Task PostDiagnosticLogs(String deviceIdentifier, { messages = logMessages }; - StringContent content = new StringContent(JsonConvert.SerializeObject(container), Encoding.UTF8, "application/json"); + StringContent content = new(StringSerialiser.Serialise(container), Encoding.UTF8, "application/json"); - HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, content, cancellationToken); + Result? result = await this.Post(requestUri, content, cancellationToken); - await this.HandleResponse(httpResponse, cancellationToken); + // TODO: return the result to the caller so that we can retry if it fails (and also log any errors that occur here) } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs index aeb5222b3..d01cacf5b 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/MerchantService.cs @@ -1,10 +1,9 @@ using System.Diagnostics.CodeAnalysis; -using System.Net.Http.Headers; -using ClientProxyBase; -using Newtonsoft.Json; +using Shared.Results; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessor.Mobile.BusinessLogic.UIServices; using TransactionProcessorACL.DataTransferObjects.Responses; using ProductType = TransactionProcessor.Mobile.BusinessLogic.Models.ProductType; @@ -39,7 +38,10 @@ public class MerchantService : ClientProxyBase.ClientProxyBase, IMerchantService public MerchantService(Func baseAddressResolver, HttpClient httpClient, IApplicationCache applicationCache, - IApplicationInfoService applicationInfoService) : base(httpClient) { + IApplicationInfoService applicationInfoService, + Func serialise, + Func deserialise) : base(httpClient, serialise, deserialise) + { this.BaseAddressResolver = baseAddressResolver; this.ApplicationCache = applicationCache; this.ApplicationInfoService = applicationInfoService; @@ -68,28 +70,19 @@ public async Task>> GetContractProducts(Cancel Logger.LogInformation("About to request merchant contracts"); Logger.LogDebug($"Merchant Contract Request details: Access Token {accessToken.AccessToken}"); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); - var httpResponse = await this.HttpClient.SendAsync(request, cancellationToken); - - // Process the response - Result content = await this.HandleResponseX(httpResponse, cancellationToken); + Result>? responseDataResult = await this.Get>(requestUri, accessToken.AccessToken, cancellationToken); - if (content.IsFailed) { - Logger.LogInformation($"GetMerchantContracts failed {content.Status}"); - return Result.Failure(content.Message); + if (responseDataResult.IsFailed) { + Logger.LogInformation($"GetMerchantContracts failed {responseDataResult.Status}"); + return ResultHelpers.CreateFailure(responseDataResult); } - Logger.LogDebug($"Transaction Response details: Status {httpResponse.StatusCode} Payload {content.Data}"); - - List? responseData = JsonConvert.DeserializeObject>(content.Data); + Logger.LogInformation($"{responseDataResult.Data.Count} for merchant requested successfully"); + Logger.LogDebug($"Merchant Contract Response: [{StringSerialiser.Serialise(responseDataResult.Data)}]"); - Logger.LogInformation($"{responseData.Count} for merchant requested successfully"); - Logger.LogDebug($"Merchant Contract Response: [{JsonConvert.SerializeObject(responseData)}]"); - - foreach (ContractResponse contractResponse in responseData) { + foreach (ContractResponse contractResponse in responseDataResult.Data) { foreach (ContractProduct contractResponseProduct in contractResponse.Products) { - var productType = GetProductType(contractResponse.OperatorName); + ProductType productType = GetProductType(contractResponse.OperatorName); models.Add(new ContractProductModel { OperatorId = contractResponse.OperatorId, @@ -142,48 +135,57 @@ public async Task> GetMerchantDetails(CancellationT Logger.LogInformation("About to request merchant details"); Logger.LogDebug($"Merchant Details Request details: Access Token {accessToken.AccessToken}"); - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); - var httpResponse = await this.HttpClient.SendAsync(request, cancellationToken); + //HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUri); + //request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); + //var httpResponse = await this.HttpClient.SendAsync(request, cancellationToken); + + //// Process the response + //Result content = await this.HandleResponseX(httpResponse, cancellationToken); - // Process the response - Result content = await this.HandleResponseX(httpResponse, cancellationToken); + //if (content.IsFailed) + //{ + // Logger.LogInformation($"GetMerchantContracts failed {content.Status}"); + // return Result.Failure(content.Message); + //} - if (content.IsFailed) + //Logger.LogDebug($"Transaction Response details: Status {httpResponse.StatusCode} Payload {content.Data}"); + + ////ResponseData responseData = this.HandleResponseContent(content.Data); + //MerchantResponse responseData = JsonConvert.DeserializeObject(content.Data); + + Result? responseDataResult = await this.Get(requestUri, accessToken.AccessToken, cancellationToken); + + if (responseDataResult.IsFailed) { - Logger.LogInformation($"GetMerchantContracts failed {content.Status}"); - return Result.Failure(content.Message); + Logger.LogInformation($"GetMerchantContracts failed {responseDataResult.Status}"); + return ResultHelpers.CreateFailure(responseDataResult); } - Logger.LogDebug($"Transaction Response details: Status {httpResponse.StatusCode} Payload {content.Data}"); - - //ResponseData responseData = this.HandleResponseContent(content.Data); - MerchantResponse responseData = JsonConvert.DeserializeObject(content.Data); Logger.LogInformation("Merchant details requested successfully"); - Logger.LogDebug($"Merchant Details Response: [{JsonConvert.SerializeObject(responseData)}]"); - + Logger.LogDebug($"Merchant Details Response: [{StringSerialiser.Serialise(responseDataResult.Data)}]"); + MerchantDetailsModel model = new MerchantDetailsModel { - EstateId = responseData.EstateId, - MerchantId = responseData.MerchantId, - MerchantName = responseData.MerchantName, - NextStatementDate = responseData.NextStatementDate, + EstateId = responseDataResult.Data.EstateId, + MerchantId = responseDataResult.Data.MerchantId, + MerchantName = responseDataResult.Data.MerchantName, + NextStatementDate = responseDataResult.Data.NextStatementDate, LastStatementDate = new DateTime(), - SettlementSchedule = responseData.SettlementSchedule.ToString(), + SettlementSchedule = responseDataResult.Data.SettlementSchedule.ToString(), //AvailableBalance = merchantResponse.AvailableBalance, //Balance = merchantResponse.Balance, Contact = new ContactModel { - Name = responseData.Contacts.First().ContactName, - EmailAddress = responseData.Contacts.First().ContactEmailAddress, - MobileNumber = responseData.Contacts.First().ContactPhoneNumber + Name = responseDataResult.Data.Contacts.First().ContactName, + EmailAddress = responseDataResult.Data.Contacts.First().ContactEmailAddress, + MobileNumber = responseDataResult.Data.Contacts.First().ContactPhoneNumber }, Address = new AddressModel { - AddressLine3 = responseData.Addresses.First().AddressLine3, - Town = responseData.Addresses.First().Town, - AddressLine4 = responseData.Addresses.First().AddressLine4, - PostalCode = responseData.Addresses.First().PostalCode, - Region = responseData.Addresses.First().Region, - AddressLine1 = responseData.Addresses.First().AddressLine1, - AddressLine2 = responseData.Addresses.First().AddressLine2 + AddressLine3 = responseDataResult.Data.Addresses.First().AddressLine3, + Town = responseDataResult.Data.Addresses.First().Town, + AddressLine4 = responseDataResult.Data.Addresses.First().AddressLine4, + PostalCode = responseDataResult.Data.Addresses.First().PostalCode, + Region = responseDataResult.Data.Addresses.First().Region, + AddressLine1 = responseDataResult.Data.Addresses.First().AddressLine1, + AddressLine2 = responseDataResult.Data.Addresses.First().AddressLine2 } }; diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/TransactionService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/TransactionService.cs index 0db1abe04..4fe652161 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/TransactionService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/TransactionService.cs @@ -1,12 +1,8 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Text; -using ClientProxyBase; -using Newtonsoft.Json; -using SimpleResults; +using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Common; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; using TransactionProcessorACL.DataTransferObjects; using TransactionProcessorACL.DataTransferObjects.Responses; @@ -47,12 +43,10 @@ public class TransactionService : ClientProxyBase.ClientProxyBase, ITransactionS public TransactionService(Func baseAddressResolver, HttpClient httpClient, - IApplicationCache applicationCache) : base(httpClient) { + IApplicationCache applicationCache, Func serialise, + Func deserialise) : base(httpClient, serialise, deserialise) { this.BaseAddressResolver = baseAddressResolver; this.ApplicationCache = applicationCache; - - // Add the API version header - this.HttpClient.DefaultRequestHeaders.Add("api-version", "1.0"); } #endregion @@ -189,15 +183,6 @@ public async Task> PerformVoucherIssue( return Result.Success(responseModel); } - - protected override async Task> HandleResponseX(HttpResponseMessage responseMessage, - CancellationToken cancellationToken) { - if (responseMessage.StatusCode == HttpStatusCode.HttpVersionNotSupported) { - throw new ApplicationException("Application needs to be updated to the latest version"); - } - - return await base.HandleResponseX(responseMessage, cancellationToken); - } private String BuildRequestUrl(String route) { String baseAddress = this.BaseAddressResolver("TransactionProcessorACL"); @@ -212,33 +197,23 @@ private async Task> SendTransactionRequest result = await this.HandleResponseX(httpResponse, cancellationToken); + Result? result = await this.Post(requestUri, request, accessToken.AccessToken, cancellationToken); if (result.IsSuccess == false) { - Logger.LogWarning("Error performing Voucher transaction"); - return Result.Failure("Error performing Voucher transaction"); + Logger.LogWarning("Error performing transaction"); + return Result.Failure("Error performing transaction"); } - Logger.LogDebug($"Transaction Response details: Status {httpResponse.StatusCode} Payload {result.Data}"); - - var responseData = JsonConvert.DeserializeObject(result.Data); + Logger.LogDebug($"Transaction Response details: Payload {StringSerialiser.Serialise(result.Data)}"); - return Result.Success(responseData); + return Result.Success(result.Data); } catch (Exception ex) { // An exception has occurred, add some additional information to the message diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/UpdateService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/UpdateService.cs index 0f2db93dd..de7e3135a 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/UpdateService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/UpdateService.cs @@ -1,8 +1,8 @@ -using System.Text; -using Newtonsoft.Json; +using Shared.Results; using SimpleResults; using TransactionProcessor.Mobile.BusinessLogic.Logging; using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; namespace TransactionProcessor.Mobile.BusinessLogic.Services; @@ -25,7 +25,8 @@ private sealed record ApplicationUpdateCheckRequest(String ApplicationVersion, private readonly Func BaseAddressResolver; public UpdateService(Func baseAddressResolver, - HttpClient httpClient) : base(httpClient) + HttpClient httpClient, Func serialise, + Func deserialise) : base(httpClient, serialise, deserialise) { this.BaseAddressResolver = baseAddressResolver; } @@ -44,22 +45,16 @@ public async Task> CheckForUpdates(String Logger.LogInformation($"About to check for application updates for device identifier {deviceIdentifier}"); Logger.LogDebug($"Application update request details: Uri {requestUri}"); - using StringContent content = new(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json"); - using HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, content, cancellationToken); - Logger.LogDebug($"Application update response [{httpResponse.StatusCode}]"); + Result? result = await this.Post(requestUri, request, cancellationToken); - String responseContent = await this.HandleResponse(httpResponse, cancellationToken); - Logger.LogDebug($"Application update response content [{responseContent}]"); - - ApplicationUpdateCheckResponse? response = JsonConvert.DeserializeObject(responseContent); - - if (response == null) - { - return Result.Failure("Application update check did not return a valid response."); + if (result.IsFailed) { + return ResultHelpers.CreateFailure(result); } + Logger.LogDebug($"Application update response content [{StringSerialiser.Serialise(result.Data)}]"); + Logger.LogInformation($"Application update check for device identifier {deviceIdentifier} completed successfully"); - return Result.Success(response); + return Result.Success(result.Data); } catch (Exception ex) { diff --git a/TransactionProcessor.Mobile.BusinessLogic/TransactionProcessor.Mobile.BusinessLogic.csproj b/TransactionProcessor.Mobile.BusinessLogic/TransactionProcessor.Mobile.BusinessLogic.csproj index 7d324aaa4..f5dc54271 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/TransactionProcessor.Mobile.BusinessLogic.csproj +++ b/TransactionProcessor.Mobile.BusinessLogic/TransactionProcessor.Mobile.BusinessLogic.csproj @@ -7,7 +7,7 @@ - + @@ -15,8 +15,10 @@ - - + + all + + diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs index cec32e585..b565d1f10 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs @@ -1,8 +1,10 @@ using CommunityToolkit.Mvvm.Input; using MediatR; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using MvvmHelpers.Commands; +using Newtonsoft.Json.Linq; using SimpleResults; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -115,7 +117,7 @@ private async Task> GetUserToken() { return tokenResult; } - + private async Task> PerformLogonTransaction() { // Logon Transaction TransactionCommands.PerformLogonCommand command = new(DateTime.Now); diff --git a/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs b/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs index 5eeb324ea..1143277cc 100644 --- a/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs +++ b/TransactionProcessor.Mobile.UITests/Common/DockerHelper.cs @@ -1,7 +1,4 @@ -using System.Net; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Builders; using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Networks; using EventStore.Client; @@ -10,7 +7,11 @@ using Shared.IntegrationTesting; using Shared.IntegrationTesting.TestContainers; using Shared.Logger; +using Shared.Serialisation; using Shouldly; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; using TransactionProcessor.Client; using TransactionProcessor.DataTransferObjects.Responses.Contract; using TransactionProcessor.IntegrationTesting.Helpers; @@ -54,6 +55,7 @@ public class DockerHelper : global::Shared.IntegrationTesting.TestContainers.Doc public DockerHelper() { this.TestingContext = new TestingContext(); + StringSerialiser.Initialise((IStringSerialiser)new SystemTextJsonSerializer(SystemTextJsonSerializer.GetDefaultJsonSerializerOptions())); } #endregion @@ -107,6 +109,16 @@ public override async Task CreateSubscriptions() } } + String Serialise(Object arg) + { + return StringSerialiser.Serialise(arg, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + + Object Deserialise(String arg, Type type) + { + return StringSerialiser.DeserializeObject(arg, type, new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + } + /// /// Starts the containers for scenario run. /// @@ -144,8 +156,8 @@ public override async Task StartContainersForScenarioRun(String scenarioName, Do }; HttpClient httpClient = new HttpClient(clientHandler); - this.SecurityServiceClient = new SecurityServiceClient(this.SecurityServiceBaseAddressResolver, httpClient); - this.TransactionProcessorClient = new TransactionProcessorClient(this.TransactionProcessorBaseAddressResolver, httpClient); + this.SecurityServiceClient = new SecurityServiceClient(this.SecurityServiceBaseAddressResolver, httpClient, Serialise, Deserialise); + this.TransactionProcessorClient = new TransactionProcessorClient(this.TransactionProcessorBaseAddressResolver, httpClient, Serialise, Deserialise); this.HttpClient = new HttpClient(); this.HttpClient.BaseAddress = new Uri(this.TransactionProcessorAclBaseAddressResolver(string.Empty)); @@ -234,7 +246,7 @@ public TestingContext() this.Estates = new List(); this.Clients = new List(); this.Users = new Dictionary(); - this.Roles = new Dictionary(); + this.Roles = new Dictionary(); this.ApiResources = new List(); } @@ -266,7 +278,7 @@ public TestingContext() /// public NlogLogger Logger { get; set; } public Dictionary Users; - public Dictionary Roles; + public Dictionary Roles; public List ApiResources; //public List IdentityResources; diff --git a/TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs b/TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs index f11892f1c..1f2da0874 100644 --- a/TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs +++ b/TransactionProcessor.Mobile.UITests/Steps/SharedSteps.cs @@ -1,14 +1,15 @@ -using System.Net; -using System.Text; -using System.Text.Json; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Reqnroll; -using SecurityService.DataTransferObjects.Requests; +using SecurityService.DataTransferObjects; using SecurityService.IntegrationTesting.Helpers; using Shared.IntegrationTesting; +using Shared.Serialisation; using Shouldly; -using TransactionProcessor.Database.Contexts; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text; +using System.Text.Json; using TransactionProcessor.DataTransferObjects.Requests.Contract; using TransactionProcessor.DataTransferObjects.Requests.Estate; using TransactionProcessor.DataTransferObjects.Requests.Merchant; @@ -19,6 +20,7 @@ using TransactionProcessor.IntegrationTesting.Helpers; using TransactionProcessor.Mobile.UITests.Common; using TransactionProcessor.Mobile.UITests.Pages; +using TransactionProcessor.ProjectionEngine.Database.Database.Entities; using AssignOperatorRequest = TransactionProcessor.DataTransferObjects.Requests.Estate.AssignOperatorRequest; namespace TransactionProcessor.Mobile.UITests.Steps @@ -51,9 +53,9 @@ public SharedSteps(TestingContext testingContext) { public async Task GivenTheFollowingSecurityRolesExist(DataTable table) { List requests = table.Rows.ToCreateRoleRequests(); - List<(String, Guid)> responses = await this.SecurityServiceSteps.GivenICreateTheFollowingRoles(requests, CancellationToken.None); + List<(String, String)> responses = await this.SecurityServiceSteps.GivenICreateTheFollowingRoles(requests, CancellationToken.None); - foreach ((String, Guid) response in responses) + foreach ((String, String) response in responses) { this.TestingContext.Roles.Add(response.Item1, response.Item2); } @@ -199,27 +201,27 @@ public async Task GivenIHaveCreatedAConfigForMyDevice() { ClientDetails clientDetails = this.TestingContext.GetClientDetails("mobileAppClient"); //ClientDetails clientDetails = ClientDetails.Create("clientId-mobileAppClient", "secret-mobile", new List()); var configRequest = new { - clientId = clientDetails.ClientId, - clientSecret = clientDetails.ClientSecret, - deviceIdentifier = deviceSerial, + client_id = clientDetails.ClientId, + client_secret = clientDetails.ClientSecret, + device_identifier = deviceSerial, id = deviceSerial, - enableAutoUpdates = false, - logLevel = 3, - hostAddresses = new List() + enable_auto_updates = false, + log_level = 3, + host_addresses = new List() }; - configRequest.hostAddresses.Add(new + configRequest.host_addresses.Add(new { servicetype = 1, uri = this.TestingContext.DockerHelper.SecurityServiceBaseAddressResolver("").Replace("127.0.0.1", this.TestingContext.DockerHelper.LocalIPAddress) }); - configRequest.hostAddresses.Add(new + configRequest.host_addresses.Add(new { - servicetype = 2, + service_type = 2, uri = this.TestingContext.DockerHelper.TransactionProcessorAclBaseAddressResolver("").Replace("127.0.0.1", this.TestingContext.DockerHelper.LocalIPAddress) }); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"http://127.0.0.1:{this.TestingContext.DockerHelper.ConfigHostPort}/api/transactionmobileconfiguration"); - request.Content = new StringContent(JsonConvert.SerializeObject(configRequest), Encoding.UTF8, "application/json"); + request.Content = new StringContent(StringSerialiser.Serialise(configRequest), Encoding.UTF8, "application/json"); HttpClientHandler clientHandler = new HttpClientHandler { @@ -274,10 +276,8 @@ public async Task GivenIHaveCreatedAConfigForMyApplication() private async Task GetMerchantBalance(Guid merchantId) { - JsonElement jsonElement = (JsonElement)await this.TestingContext.DockerHelper.ProjectionManagementClient.GetStateAsync("MerchantBalanceProjection", $"MerchantBalance-{merchantId:N}"); - JObject jsonObject = JObject.Parse(jsonElement.GetRawText()); - decimal balanceValue = jsonObject.SelectToken("merchant.balance").Value(); - return balanceValue; + MerchantBalanceProjectionState1 balanceState = await this.TestingContext.DockerHelper.ProjectionManagementClient.GetStateAsync("MerchantBalanceProjection", $"MerchantBalance-{merchantId:N}"); + return balanceState.merchant.balance; } [Given(@"I make the following manual merchant deposits")] @@ -342,7 +342,31 @@ public async Task GivenTheFollowingUsersAreAvailableAtThePataPawaPrePayHost(Data public async Task GivenTheFollowingMetersAreAvailableAtThePataPawaPrePayHost(DataTable table) { List meters = table.Rows.ToPataPawaMeters(); - await this.TransactionProcessorSteps.GivenTheFollowingMetersAreAvailableAtThePataPawaPrePaidHost(meters); + await this.GivenTheFollowingMetersAreAvailableAtThePataPawaPrePaidHost(meters); + } + + public async Task GivenTheFollowingMetersAreAvailableAtThePataPawaPrePaidHost(List meters) + { + await this.SendRequestToTestHost(meters, "/api/developer/patapawaprepay/createmeter"); + } + + public async Task SendRequestToTestHost(List objects, String url) + { + foreach (T o in objects) + { + HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url); + + httpRequestMessage.Content = new StringContent(StringSerialiser.Serialise(o, new SerialiserOptions(SerialiserPropertyFormat.CamelCase)), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await this.TestingContext.DockerHelper.TestHostHttpClient.SendAsync(httpRequestMessage); + response.StatusCode.ShouldBe(HttpStatusCode.OK); + } } } } + +[ExcludeFromCodeCoverage] +public record Merchant(string Id, string Name, int numberOfEventsProcessed, decimal balance); + +[ExcludeFromCodeCoverage] +public record MerchantBalanceProjectionState1(Merchant merchant); diff --git a/TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj b/TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj index d8940fac9..8379eacfe 100644 --- a/TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj +++ b/TransactionProcessor.Mobile.UITests/TransactionProcessor.Mobile.UITests.csproj @@ -16,7 +16,7 @@ - + @@ -28,11 +28,11 @@ - - - - - + + + + + diff --git a/TransactionProcessor.Mobile/AppShell.xaml b/TransactionProcessor.Mobile/AppShell.xaml index 30490d028..f3e436969 100644 --- a/TransactionProcessor.Mobile/AppShell.xaml +++ b/TransactionProcessor.Mobile/AppShell.xaml @@ -11,6 +11,7 @@ xmlns:myAccount="clr-namespace:TransactionProcessor.Mobile.Pages.MyAccount" xmlns:support="clr-namespace:TransactionProcessor.Mobile.Pages.Support" Title="TransactionProcessor.Mobile" + Shell.FlyoutBehavior="Disabled" Shell.TabBarBackgroundColor="{DynamicResource shellTabBarBackgroundColor}" Shell.TabBarForegroundColor="{DynamicResource shellTabBarForegroundColor}" Shell.TabBarTitleColor="{DynamicResource shellTabBarForegroundColor}" diff --git a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs index 8fd93c1d7..bbbb3379b 100644 --- a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs +++ b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs @@ -1,11 +1,8 @@ using System.Net.Security; using banditoth.MAUI.DeviceId; -using MediatR; using SecurityService.Client; -using SimpleResults; using TransactionMobile.Maui.UIServices; using TransactionProcessor.Mobile.BusinessLogic.Database; -using TransactionProcessor.Mobile.BusinessLogic.Models; using TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; using TransactionProcessor.Mobile.BusinessLogic.Services; using TransactionProcessor.Mobile.BusinessLogic.Services.TrainingModeServices; @@ -26,10 +23,8 @@ using TransactionProcessor.Mobile.Pages.Transactions.BillPayment; using TransactionProcessor.Mobile.Pages.Transactions.MobileTopup; using TransactionProcessor.Mobile.Pages.Transactions.Voucher; -//using TransactionProcessor.Mobile.Platforms.Android; using TransactionProcessor.Mobile.UIServices; -using LogMessage = TransactionProcessor.Mobile.BusinessLogic.Models.LogMessage; -using ClientProxyBase; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; #if ANDROID @@ -94,7 +89,15 @@ public static MauiAppBuilder ConfigureAppServices(this MauiAppBuilder builder) { builder.Services.RegisterHttpClientX(); builder.Services.AddSingleton(); - builder.ConfigureDeviceIdProvider(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton>(_ => obj => StringSerialiser.Serialise(obj)); + builder.Services.AddSingleton>(_ => (str, type) => StringSerialiser.DeserializeObject(str, type)); + + var serialiserSettings = SystemTextJsonSerializer.GetDefaultJsonSerializerOptions(); + + builder.Services.AddSingleton(serialiserSettings); + + builder.ConfigureDeviceIdProvider(); return builder; } diff --git a/TransactionProcessor.Mobile/MauiProgram.cs b/TransactionProcessor.Mobile/MauiProgram.cs index 2013cef1e..4218e814e 100644 --- a/TransactionProcessor.Mobile/MauiProgram.cs +++ b/TransactionProcessor.Mobile/MauiProgram.cs @@ -8,6 +8,7 @@ using TransactionProcessor.Mobile.Extensions; using TransactionProcessor.Mobile.UIServices; using Microsoft.Extensions.DependencyInjection; +using TransactionProcessor.Mobile.BusinessLogic.Serialisation; namespace TransactionProcessor.Mobile { @@ -34,6 +35,9 @@ public static MauiApp CreateMauiApp() Container = builder.Build(); + var serialiser = Container.Services.GetRequiredService(); + StringSerialiser.Initialise(serialiser); + Logger.Initialise(new ConsoleLogger()); return Container; diff --git a/TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj b/TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj index 8d2d85a36..eab23a0cd 100644 --- a/TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj +++ b/TransactionProcessor.Mobile/TransactionProcessor.Mobile.csproj @@ -151,22 +151,17 @@ - - - - - + + + + - - - - ANDROID