diff --git a/.github/workflows/pullrequest_android.yml b/.github/workflows/pullrequest_android.yml index dbedfad1..d9b97781 100644 --- a/.github/workflows/pullrequest_android.yml +++ b/.github/workflows/pullrequest_android.yml @@ -19,6 +19,12 @@ jobs: - name: Checkout uses: actions/checkout@v2 + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v1.1 + with: + vs-prerelease: true + msbuild-architecture: x64 + - name: Setup .NET 6 uses: actions/setup-dotnet@v1 with: @@ -32,8 +38,12 @@ jobs: - name: Install MAUI Workloads run: | - dotnet workload install android --ignore-failed-sources dotnet workload install maui --ignore-failed-sources + dotnet workload install maui-android --ignore-failed-sources + dotnet workload install maui-desktop --ignore-failed-sources + dotnet workload install maui-ios --ignore-failed-sources + dotnet workload install maui-mobile --ignore-failed-sources + dotnet workload install maui-windows --ignore-failed-sources - name: Restore Dependencies run: dotnet restore TransactionMobile.Maui.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json diff --git a/.github/workflows/pullrequest_ios.yml b/.github/workflows/pullrequest_ios.yml index 07db90f8..b1b19c6e 100644 --- a/.github/workflows/pullrequest_ios.yml +++ b/.github/workflows/pullrequest_ios.yml @@ -25,7 +25,11 @@ jobs: dotnet-version: 6.0.200-preview.22055.15 include-prerelease: true - #- run: dotnet tool update -g dotnet-vs + - name: Setup .NET 6 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 6.0.x + include-prerelease: true - name: Install MAUI Workloads run: | @@ -33,7 +37,7 @@ jobs: dotnet workload install maui --ignore-failed-sources - name: Restore Dependencies - run: dotnet restore TransactionMobile.Maui.sln + run: dotnet restore TransactionMobile.Maui.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json - name: Build Code run: dotnet build TransactionMobile.Maui/TransactionMobile.Maui.csproj -c Release -f net6.0-ios --no-restore diff --git a/.github/workflows/pullrequest_maccatalyst.yml b/.github/workflows/pullrequest_maccatalyst.yml index 2345c252..376c565d 100644 --- a/.github/workflows/pullrequest_maccatalyst.yml +++ b/.github/workflows/pullrequest_maccatalyst.yml @@ -39,7 +39,7 @@ jobs: dotnet workload install maui --ignore-failed-sources - name: Restore Dependencies - run: dotnet restore TransactionMobile.Maui.sln + run: dotnet restore TransactionMobile.Maui.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json - name: Build Code run: dotnet build TransactionMobile.Maui/TransactionMobile.Maui.csproj -c Release -f net6.0-maccatalyst --no-restore diff --git a/.github/workflows/pullrequest_windows.yml b/.github/workflows/pullrequest_windows.yml index 5582c4f3..2bb53242 100644 --- a/.github/workflows/pullrequest_windows.yml +++ b/.github/workflows/pullrequest_windows.yml @@ -37,6 +37,9 @@ jobs: - name: Install MAUI Workloads run: | dotnet workload install maui --ignore-failed-sources + dotnet workload install maui-desktop --ignore-failed-sources + dotnet workload install maui-mobile --ignore-failed-sources + dotnet workload install maui-windows --ignore-failed-sources - name: Restore Dependencies run: dotnet restore TransactionMobile.Maui.sln --source https://api.nuget.org/v3/index.json --source https://www.myget.org/F/transactionprocessing/api/v3/index.json --source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json diff --git a/TransactionMobile.Maui.BusinessLogic.Tests/RequestHandlerTests/MerchantRequestHandlerTests.cs b/TransactionMobile.Maui.BusinessLogic.Tests/RequestHandlerTests/MerchantRequestHandlerTests.cs index 8ce8b043..1a8bfd6c 100644 --- a/TransactionMobile.Maui.BusinessLogic.Tests/RequestHandlerTests/MerchantRequestHandlerTests.cs +++ b/TransactionMobile.Maui.BusinessLogic.Tests/RequestHandlerTests/MerchantRequestHandlerTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using Models; using Moq; using RequestHandlers; @@ -18,14 +19,14 @@ public class MerchantRequestHandlerTests public async Task MerchantRequestHandler_GetContractProductsRequest_Handle_IsHandled() { Mock merchantService = new Mock(); - merchantService.Setup(m => m.GetContractProducts(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + merchantService.Setup(m => m.GetContractProducts(It.IsAny())) .ReturnsAsync(TestData.ContractProductList); - MerchantRequestHandler handler = new MerchantRequestHandler(merchantService.Object); + Mock memoryCacheService = new Mock(); + - GetContractProductsRequest request = GetContractProductsRequest.Create(TestData.Token, - TestData.EstateId, - TestData.MerchantId, - null); + MerchantRequestHandler handler = new MerchantRequestHandler(merchantService.Object, memoryCacheService.Object); + + GetContractProductsRequest request = GetContractProductsRequest.Create(); List contractProductModels = await handler.Handle(request, CancellationToken.None); @@ -36,13 +37,12 @@ public async Task MerchantRequestHandler_GetContractProductsRequest_Handle_IsHan public async Task MerchantRequestHandler_GetMerchantBalanceRequest_Handle_IsHandled() { Mock merchantService = new Mock(); - merchantService.Setup(m => m.GetMerchantBalance(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + merchantService.Setup(m => m.GetMerchantBalance(It.IsAny())) .ReturnsAsync(TestData.MerchantBalance); - MerchantRequestHandler handler = new MerchantRequestHandler(merchantService.Object); + Mock memoryCacheService = new Mock(); + MerchantRequestHandler handler = new MerchantRequestHandler(merchantService.Object, memoryCacheService.Object); - GetMerchantBalanceRequest request = GetMerchantBalanceRequest.Create(TestData.Token, - TestData.EstateId, - TestData.MerchantId); + GetMerchantBalanceRequest request = GetMerchantBalanceRequest.Create(); Decimal merchantBalance = await handler.Handle(request, CancellationToken.None); diff --git a/TransactionMobile.Maui.BusinessLogic.Tests/RequestTests/RequestTests.cs b/TransactionMobile.Maui.BusinessLogic.Tests/RequestTests/RequestTests.cs index 2febcc4e..10932226 100644 --- a/TransactionMobile.Maui.BusinessLogic.Tests/RequestTests/RequestTests.cs +++ b/TransactionMobile.Maui.BusinessLogic.Tests/RequestTests/RequestTests.cs @@ -15,28 +15,31 @@ public class RequestTests [Fact] public void GetContractProductsRequest_Create_IsCreated() { - GetContractProductsRequest request = GetContractProductsRequest.Create(TestData.Token, - TestData.EstateId, - TestData.MerchantId, - null); + GetContractProductsRequest request = GetContractProductsRequest.Create(); request.ShouldNotBeNull(); - request.AccessToken.ShouldBe(TestData.Token); - request.EstateId.ShouldBe(TestData.EstateId); - request.MerchantId.ShouldBe(TestData.MerchantId); + request.ProductType.ShouldBeNull(); + } + + [Theory] + [InlineData(Models.ProductType.BillPayment)] + [InlineData(Models.ProductType.MobileWallet)] + [InlineData(Models.ProductType.MobileTopup)] + [InlineData(Models.ProductType.Voucher)] + public void GetContractProductsRequest_Create_WithProductType_IsCreated(Models.ProductType productType) + { + GetContractProductsRequest request = GetContractProductsRequest.Create(productType); + + request.ShouldNotBeNull(); + request.ProductType.ShouldBe(productType); } [Fact] public void GetMerchantBalanceRequest_Create_IsCreated() { - GetMerchantBalanceRequest request = GetMerchantBalanceRequest.Create(TestData.Token, - TestData.EstateId, - TestData.MerchantId); + GetMerchantBalanceRequest request = GetMerchantBalanceRequest.Create(); request.ShouldNotBeNull(); - request.AccessToken.ShouldBe(TestData.Token); - request.EstateId.ShouldBe(TestData.EstateId); - request.MerchantId.ShouldBe(TestData.MerchantId); } [Fact] diff --git a/TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj b/TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj index 5d479578..169847c6 100644 --- a/TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj +++ b/TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/AdminPageViewModelTests.cs b/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/AdminPageViewModelTests.cs index 9f51c7e0..d1b8c6ee 100644 --- a/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/AdminPageViewModelTests.cs +++ b/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/AdminPageViewModelTests.cs @@ -14,22 +14,21 @@ namespace TransactionMobile.Maui.BusinessLogic.Tests.ViewModelTests using ViewModels.Admin; using ViewModels.Transactions; using Xunit; + using TransactionMobile.Maui.BusinessLogic.Services; public class AdminPageViewModelTests { [Fact] - public void TransactionsPageViewModel_AdminCommand_Execute_IsExecuted() + public void AdminPageViewModel_AdminCommand_Execute_IsExecuted() { Mock navigationService = new Mock(); Mock mediator = new Mock(); - Mock userDetailsCache = new Mock(); - Mock configurationCache = new Mock(); + Mock memoryCacheService = new Mock(); Mock deviceService = new Mock(); Mock applicationInfoService = new Mock(); AdminPageViewModel viewModel = new AdminPageViewModel(mediator.Object, navigationService.Object, - userDetailsCache.Object, - configurationCache.Object, + memoryCacheService.Object, deviceService.Object, applicationInfoService.Object); diff --git a/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs b/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs index 85328ced..83c3a685 100644 --- a/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs +++ b/TransactionMobile.Maui.BusinessLogic.Tests/ViewModelTests/LoginPageViewModelTests.cs @@ -8,6 +8,7 @@ namespace TransactionMobile.Maui.BusinessLogic.Tests.ViewModelTests; using Models; using Moq; using Requests; +using TransactionMobile.Maui.BusinessLogic.Services; using UIServices; using ViewModels; using Xunit; @@ -19,11 +20,10 @@ public void LoginPageViewModel_LoginCommand_Execute_IsExecuted() { Mock mediator = new Mock(); Mock navigationService = new Mock(); - MemoryCache userDetailsCache = new MemoryCache(new MemoryCacheOptions()); - IMemoryCache configurationCache = new MemoryCache(new MemoryCacheOptions()); + Mock memoryCacheService = new Mock(); Mock deviceService = new Mock(); Mock applicationInfoService = new Mock(); - LoginPageViewModel viewModel = new LoginPageViewModel(mediator.Object, navigationService.Object,userDetailsCache,configurationCache, + LoginPageViewModel viewModel = new LoginPageViewModel(mediator.Object, navigationService.Object, memoryCacheService.Object, deviceService.Object,applicationInfoService.Object); mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())).ReturnsAsync(new Configuration()); diff --git a/TransactionMobile.Maui.BusinessLogic/Models/Configuration.cs b/TransactionMobile.Maui.BusinessLogic/Models/Configuration.cs index d80dede3..23fa96dd 100644 --- a/TransactionMobile.Maui.BusinessLogic/Models/Configuration.cs +++ b/TransactionMobile.Maui.BusinessLogic/Models/Configuration.cs @@ -6,13 +6,13 @@ public class Configuration public String ClientSecret { get; set; } - public String SecurityServiceUrl { get; set; } + public String SecurityServiceUri { get; set; } - public String TransactionProcessorAclUrl { get; set; } + public String TransactionProcessorAclUri { get; set; } - public String EstateManagementUrl { get; set; } + public String EstateManagementUri { get; set; } - public String EstateReportingUrl { get; set; } + public String EstateReportingUri { get; set; } public LogLevel LogLevel { get; set; } diff --git a/TransactionMobile.Maui.BusinessLogic/Models/TokenResponseModel.cs b/TransactionMobile.Maui.BusinessLogic/Models/TokenResponseModel.cs index 8a406357..6a8f9129 100644 --- a/TransactionMobile.Maui.BusinessLogic/Models/TokenResponseModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/Models/TokenResponseModel.cs @@ -10,5 +10,5 @@ public class TokenResponseModel { public String AccessToken { get; set; } public String RefreshToken { get; set; } - public Int32 ExpiryInMinutes { get; set; } + public Int64 ExpiryInMinutes { get; set; } } \ No newline at end of file diff --git a/TransactionMobile.Maui.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs b/TransactionMobile.Maui.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs index 2854e0f8..b4ba528a 100644 --- a/TransactionMobile.Maui.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs +++ b/TransactionMobile.Maui.BusinessLogic/RequestHandlers/MerchantRequestHandler.cs @@ -1,6 +1,8 @@ namespace TransactionMobile.Maui.BusinessLogic.RequestHandlers; using MediatR; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Primitives; using Models; using Requests; using Services; @@ -11,12 +13,15 @@ public class MerchantRequestHandler : IRequestHandler> Handle(GetContractProductsRequest request, CancellationToken cancellationToken) { - List products = await this.MerchantService.GetContractProducts(request.AccessToken, request.EstateId, request.MerchantId, cancellationToken); + this.MemoryCacheService.TryGetValue>("ContractProducts", out List products); + + if (products == null || products.Any() == false) + { + products = await this.MerchantService.GetContractProducts(cancellationToken); + + this.CacheContractData(products); + } if (request.ProductType.HasValue) { @@ -39,7 +51,22 @@ public async Task> Handle(GetContractProductsRequest public async Task Handle(GetMerchantBalanceRequest request, CancellationToken cancellationToken) { - return await this.MerchantService.GetMerchantBalance(request.AccessToken, request.EstateId, request.MerchantId, cancellationToken); + return await this.MerchantService.GetMerchantBalance(cancellationToken); + } + + private void CacheContractData(List contractProductModels) + { + DateTime expirationTime = DateTime.Now.AddMinutes(60); + CancellationChangeToken expirationToken = new CancellationChangeToken(new CancellationTokenSource(TimeSpan.FromMinutes(60)).Token); + MemoryCacheEntryOptions cacheEntryOptions = new MemoryCacheEntryOptions() + // Pin to cache. + .SetPriority(CacheItemPriority.NeverRemove) + // Set the actual expiration time + .SetAbsoluteExpiration(expirationTime) + // Force eviction to run + .AddExpirationToken(expirationToken); + + this.MemoryCacheService.Set("ContractProducts", contractProductModels, cacheEntryOptions); } #endregion diff --git a/TransactionMobile.Maui.BusinessLogic/Requests/GetContractProductsRequest.cs b/TransactionMobile.Maui.BusinessLogic/Requests/GetContractProductsRequest.cs index 8a6c51ee..e054fc4e 100644 --- a/TransactionMobile.Maui.BusinessLogic/Requests/GetContractProductsRequest.cs +++ b/TransactionMobile.Maui.BusinessLogic/Requests/GetContractProductsRequest.cs @@ -7,37 +7,22 @@ public class GetContractProductsRequest : IRequest> { #region Constructors - private GetContractProductsRequest(String accessToken, - Guid estateId, - Guid merchantId, - ProductType? productType) + private GetContractProductsRequest(ProductType? productType) { - this.AccessToken = accessToken; - this.EstateId = estateId; - this.MerchantId = merchantId; this.ProductType = productType; } #endregion #region Properties - - public String AccessToken { get; } - - public Guid EstateId { get; } - - public Guid MerchantId { get; } public ProductType? ProductType { get; } #endregion #region Methods - public static GetContractProductsRequest Create(String accessToken, - Guid estateId, - Guid merchantId, - ProductType? productType) + public static GetContractProductsRequest Create(ProductType? productType =null) { - return new GetContractProductsRequest(accessToken, estateId, merchantId,productType); + return new GetContractProductsRequest(productType); } #endregion diff --git a/TransactionMobile.Maui.BusinessLogic/Requests/GetMerchantBalanceRequest.cs b/TransactionMobile.Maui.BusinessLogic/Requests/GetMerchantBalanceRequest.cs index 8d6470c7..27721ff9 100644 --- a/TransactionMobile.Maui.BusinessLogic/Requests/GetMerchantBalanceRequest.cs +++ b/TransactionMobile.Maui.BusinessLogic/Requests/GetMerchantBalanceRequest.cs @@ -6,34 +6,21 @@ public class GetMerchantBalanceRequest : IRequest { #region Constructors - private GetMerchantBalanceRequest(String accessToken, - Guid estateId, - Guid merchantId) + private GetMerchantBalanceRequest() { - this.AccessToken = accessToken; - this.EstateId = estateId; - this.MerchantId = merchantId; + } #endregion #region Properties - - public String AccessToken { get; } - - public Guid EstateId { get; } - - public Guid MerchantId { get; } - #endregion #region Methods - public static GetMerchantBalanceRequest Create(String accessToken, - Guid estateId, - Guid merchantId) + public static GetMerchantBalanceRequest Create() { - return new GetMerchantBalanceRequest(accessToken, estateId, merchantId); + return new GetMerchantBalanceRequest(); } #endregion diff --git a/TransactionMobile.Maui.BusinessLogic/Services/AuthenticationService.cs b/TransactionMobile.Maui.BusinessLogic/Services/AuthenticationService.cs new file mode 100644 index 00000000..d38ec9bd --- /dev/null +++ b/TransactionMobile.Maui.BusinessLogic/Services/AuthenticationService.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.BusinessLogic.Services +{ + using Microsoft.Extensions.Caching.Memory; + using Models; + using SecurityService.Client; + using SecurityService.DataTransferObjects.Responses; + + public class AuthenticationService : IAuthenticationService + { + private readonly ISecurityServiceClient SecurityServiceClient; + + private readonly IMemoryCacheService MemoryCacheService; + + public AuthenticationService(ISecurityServiceClient securityServiceClient, IMemoryCacheService memoryCacheService) + { + this.SecurityServiceClient = securityServiceClient; + this.MemoryCacheService = memoryCacheService; + } + + public async Task GetToken(String username, + String password, + CancellationToken cancellationToken) + { + try + { + this.MemoryCacheService.TryGetValue("Configuration", out Configuration configuration); + + username = "merchantuser@v28emulatormerchant.co.uk"; + password = "123456"; + + TokenResponse token = + await this.SecurityServiceClient.GetToken(username, password, configuration.ClientId, configuration.ClientSecret, cancellationToken); + + return new TokenResponseModel + { + AccessToken = token.AccessToken, + ExpiryInMinutes = token.ExpiresIn, + RefreshToken = token.RefreshToken + }; + } + catch(Exception ex) + { + return null; + } + + + } + + public async Task RefreshAccessToken(String refreshToken, + CancellationToken cancellationToken) + { + this.MemoryCacheService.TryGetValue("Configuration", out Configuration configuration); + + TokenResponse token = await this.SecurityServiceClient.GetToken(configuration.ClientId,configuration.ClientSecret, refreshToken,cancellationToken); + + return new TokenResponseModel + { + AccessToken = token.AccessToken, + ExpiryInMinutes = token.ExpiresIn, + RefreshToken = token.RefreshToken + }; + } + } +} diff --git a/TransactionMobile.Maui.BusinessLogic/Services/ConfigurationService.cs b/TransactionMobile.Maui.BusinessLogic/Services/ConfigurationService.cs new file mode 100644 index 00000000..6a78ba2d --- /dev/null +++ b/TransactionMobile.Maui.BusinessLogic/Services/ConfigurationService.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.BusinessLogic.Services +{ + using System.Net; + using ClientProxyBase; + using Models; + using Newtonsoft.Json; + + public class ConfigurationService : ClientProxyBase, IConfigurationService + { + private readonly Func BaseAddressResolver; + + public ConfigurationService(Func baseAddressResolver, + HttpClient httpClient) : base(httpClient) + { + this.BaseAddressResolver = baseAddressResolver; + } + + + private String BuildRequestUrl(String route) + { + String baseAddress = this.BaseAddressResolver("ConfigServiceUrl"); + + String requestUri = $"{baseAddress}{route}"; + + return requestUri; + } + + 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; + } + + return await base.HandleResponse(responseMessage, cancellationToken); + } + + public async Task GetConfiguration(String deviceIdentifier, + CancellationToken cancellationToken) + { + Configuration response = null; + String requestUri = this.BuildRequestUrl($"/configuration/{deviceIdentifier}"); + + try + { + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.GetAsync(requestUri, cancellationToken); + + // Process the response + String content = await this.HandleResponse(httpResponse, cancellationToken); + + // call was successful so now deserialise the body to the response object + response = JsonConvert.DeserializeObject(content); + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception($"Error getting configuration for device Id {deviceIdentifier}.", ex); + + throw exception; + } + + return response; + } + + public async Task PostDiagnosticLogs(String deviceIdentifier, + List logMessages, + CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} diff --git a/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyConfigurationService.cs b/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyConfigurationService.cs index ba36e808..08e6ae3c 100644 --- a/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyConfigurationService.cs +++ b/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyConfigurationService.cs @@ -14,11 +14,11 @@ public async Task GetConfiguration(String deviceIdentifier, ClientId = "dummyClientId", ClientSecret = "dummyClientSecret", EnableAutoUpdates = false, - EstateManagementUrl = "http://localhost:5000", - EstateReportingUrl = "http://localhost:5006", + EstateManagementUri = "http://localhost:5000", + EstateReportingUri = "http://localhost:5006", LogLevel = LogLevel.Debug, - SecurityServiceUrl = "http://localhost:5001", - TransactionProcessorAclUrl = "http://localhost:5003" + SecurityServiceUri = "http://localhost:5001", + TransactionProcessorAclUri = "http://localhost:5003" }; } diff --git a/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyMerchantService.cs b/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyMerchantService.cs index f9a59c91..58bfb17e 100644 --- a/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyMerchantService.cs +++ b/TransactionMobile.Maui.BusinessLogic/Services/DummyServices/DummyMerchantService.cs @@ -4,10 +4,7 @@ public class DummyMerchantService : IMerchantService { - public async Task> GetContractProducts(String accessToken, - Guid estateId, - Guid merchantId, - CancellationToken cancellationToken) + public async Task> GetContractProducts(CancellationToken cancellationToken) { return new List { @@ -95,10 +92,7 @@ public async Task> GetContractProducts(String accessT }; } - public async Task GetMerchantBalance(String accessToken, - Guid estateId, - Guid merchantId, - CancellationToken cancellationToken) + public async Task GetMerchantBalance(CancellationToken cancellationToken) { return 100; } diff --git a/TransactionMobile.Maui.BusinessLogic/Services/IMemoryCacheService.cs b/TransactionMobile.Maui.BusinessLogic/Services/IMemoryCacheService.cs new file mode 100644 index 00000000..639d3ff3 --- /dev/null +++ b/TransactionMobile.Maui.BusinessLogic/Services/IMemoryCacheService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.BusinessLogic.Services +{ + using Microsoft.Extensions.Caching.Memory; + + public interface IMemoryCacheService + { + bool TryGetValue(string Key, out T cache); + void Set(string key, T cache); + + void Set(string key, + T cache, + MemoryCacheEntryOptions options); + } + + public class MemoryCacheService : IMemoryCacheService + { + private readonly IMemoryCache MemoryCache; + + public MemoryCacheService(IMemoryCache memoryCache) + { + this.MemoryCache = memoryCache; + } + + public void Set(string key, T cache) + { + this.Set(key, cache, default); + } + + public void Set(string key, T cache, MemoryCacheEntryOptions options) + { + this.MemoryCache.Set(key, cache, options); + } + + public bool TryGetValue(string Key, out T cache) + { + if (this.MemoryCache.TryGetValue(Key, out T cachedItem)) + { + cache = cachedItem; + return true; + } + cache = default(T); + return false; + } + } +} diff --git a/TransactionMobile.Maui.BusinessLogic/Services/IMerchantService.cs b/TransactionMobile.Maui.BusinessLogic/Services/IMerchantService.cs index fbd1614b..8db46d17 100644 --- a/TransactionMobile.Maui.BusinessLogic/Services/IMerchantService.cs +++ b/TransactionMobile.Maui.BusinessLogic/Services/IMerchantService.cs @@ -1,20 +1,106 @@ namespace TransactionMobile.Maui.BusinessLogic.Services; +using EstateManagement.Client; +using EstateManagement.DataTransferObjects.Responses; +using Microsoft.Extensions.Caching.Memory; using Models; public interface IMerchantService { #region Methods - Task> GetContractProducts(String accessToken, - Guid estateId, - Guid merchantId, - CancellationToken cancellationToken); + Task> GetContractProducts(CancellationToken cancellationToken); - Task GetMerchantBalance(String accessToken, - Guid estateId, - Guid merchantId, - CancellationToken cancellationToken); + Task GetMerchantBalance(CancellationToken cancellationToken); #endregion +} + +public class MerchantService : IMerchantService +{ + private readonly IEstateClient EstateClient; + + private readonly IMemoryCacheService MemoryCacheService; + + public MerchantService(IEstateClient estateClient, IMemoryCacheService memoryCacheService) + { + this.EstateClient = estateClient; + this.MemoryCacheService = memoryCacheService; + } + public async Task> GetContractProducts(CancellationToken cancellationToken) + { + List result = new List(); + + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.MemoryCacheService.TryGetValue("EstateId", out Guid estateId); + this.MemoryCacheService.TryGetValue("MerchantId", out Guid merchantId); + + List merchantContracts = await this.EstateClient.GetMerchantContracts(accessToken.AccessToken, estateId, merchantId, cancellationToken); + + foreach (ContractResponse contractResponse in merchantContracts) + { + foreach (ContractProduct contractResponseProduct in contractResponse.Products) + { + result.Add(new ContractProductModel + { + OperatorId = contractResponse.OperatorId, + ContractId = contractResponse.ContractId, + ProductId = contractResponseProduct.ProductId, + OperatorIdentfier = contractResponse.OperatorName, + OperatorName = this.GetOperatorName(contractResponse, contractResponseProduct), + Value = contractResponseProduct.Value ?? 0, + IsFixedValue = contractResponseProduct.Value.HasValue, + ProductDisplayText = contractResponseProduct.DisplayText, + ProductType = this.GetProductType(contractResponse.OperatorName) + }); + } + } + + return result; + } + + private String GetOperatorName(ContractResponse contractResponse, ContractProduct contractProduct) + { + String operatorName = null; + ProductType productType = this.GetProductType(contractResponse.OperatorName); + switch (productType) + { + case ProductType.Voucher: + operatorName = contractResponse.Description; + break; + default: + operatorName = contractResponse.OperatorName; + break; + + } + + return operatorName; + } + + private ProductType GetProductType(String operatorName) + { + ProductType productType = ProductType.NotSet; + switch (operatorName) + { + case "Safaricom": + productType = ProductType.MobileTopup; + break; + case "Voucher": + productType = ProductType.Voucher; + break; + } + + return productType; + } + + public async Task GetMerchantBalance(CancellationToken cancellationToken) + { + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.MemoryCacheService.TryGetValue("EstateId", out Guid estateId); + this.MemoryCacheService.TryGetValue("MerchantId", out Guid merchantId); + + MerchantBalanceResponse merchantBalance = await this.EstateClient.GetMerchantBalance(accessToken.AccessToken, estateId, merchantId, cancellationToken); + + return merchantBalance.AvailableBalance; + } } \ No newline at end of file diff --git a/TransactionMobile.Maui.BusinessLogic/Services/TransactionService.cs b/TransactionMobile.Maui.BusinessLogic/Services/TransactionService.cs new file mode 100644 index 00000000..bf41625d --- /dev/null +++ b/TransactionMobile.Maui.BusinessLogic/Services/TransactionService.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.BusinessLogic.Services +{ + using System.Net; + using System.Net.Http.Headers; + using ClientProxyBase; + using Microsoft.Extensions.Caching.Memory; + using Models; + using Newtonsoft.Json; + using TransactionProcessorACL.DataTransferObjects; + using TransactionProcessorACL.DataTransferObjects.Responses; + + public class TransactionService : ClientProxyBase, ITransactionService + { + private readonly Func BaseAddressResolver; + + private readonly IMemoryCacheService MemoryCacheService; + + private String BuildRequestUrl(String route) + { + String baseAddress = this.BaseAddressResolver("TransactionProcessorACL"); + + String requestUri = $"{baseAddress}{route}"; + + return requestUri; + } + + protected override async Task HandleResponse(HttpResponseMessage responseMessage, + CancellationToken cancellationToken) + { + if (responseMessage.StatusCode == HttpStatusCode.HttpVersionNotSupported) + { + throw new ApplicationException("Application needs to be updated to the latest version"); + } + else + { + return await base.HandleResponse(responseMessage, cancellationToken); + } + + } + + public TransactionService(Func baseAddressResolver, + HttpClient httpClient, + IMemoryCacheService memoryCacheService) : base(httpClient) + { + this.BaseAddressResolver = baseAddressResolver; + this.MemoryCacheService = memoryCacheService; + + // Add the API version header + this.HttpClient.DefaultRequestHeaders.Add("api-version", "1.0"); + } + + public async Task PerformLogon(PerformLogonRequestModel model, + CancellationToken cancellationToken) + { + PerformLogonResponseModel response = null; + + String requestUri = this.BuildRequestUrl("/api/transactions"); + + try + { + LogonTransactionRequestMessage logonTransactionRequest = new LogonTransactionRequestMessage + { + ApplicationVersion = "1.0.5", //model.ApplicationVersion, + DeviceIdentifier = model.DeviceIdentifier, + TransactionDateTime = model.TransactionDateTime, + TransactionNumber = model.TransactionNumber + }; + + String requestSerialised = JsonConvert.SerializeObject(logonTransactionRequest, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + StringContent httpContent = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + // Add the access token to the client headers + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, httpContent, cancellationToken); + + // Process the response + String content = await this.HandleResponse(httpResponse, cancellationToken); + + // call was successful so now deserialise the body to the response object + LogonTransactionResponseMessage responseMessage = JsonConvert.DeserializeObject(content); + + // Convert to model + response = new PerformLogonResponseModel + { + EstateId = responseMessage.EstateId, + MerchantId = responseMessage.MerchantId, + IsSuccessful = responseMessage.ResponseCode == "0000", + RequireApplicationUpdate = responseMessage.RequiresApplicationUpdate, + ResponseMessage = responseMessage.ResponseMessage + }; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception("Error posting logon transaction.", ex); + + throw exception; + } + + return response; + + } + + public async Task PerformMobileTopup(PerformMobileTopupRequestModel model, + CancellationToken cancellationToken) + { + Boolean response = false; + String requestUri = this.BuildRequestUrl("/api/transactions"); + + try + { + SaleTransactionRequestMessage saleTransactionRequest = new SaleTransactionRequestMessage + { + ProductId = model.ProductId, + OperatorIdentifier = model.OperatorIdentifier, + ApplicationVersion = "1.0.5", //model.ApplicationVersion, + DeviceIdentifier = model.DeviceIdentifier, + ContractId = model.ContractId, + TransactionDateTime = model.TransactionDateTime, + CustomerEmailAddress = model.CustomerEmailAddress, + TransactionNumber = model.TransactionNumber + }; + + // Add the additional request data + saleTransactionRequest.AdditionalRequestMetaData = new Dictionary(); + saleTransactionRequest.AdditionalRequestMetaData.Add("Amount", model.TopupAmount.ToString()); + saleTransactionRequest.AdditionalRequestMetaData.Add("CustomerAccountNumber", model.CustomerAccountNumber); + + String requestSerialised = JsonConvert.SerializeObject(saleTransactionRequest, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + StringContent httpContent = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + // Add the access token to the client headers + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, httpContent, cancellationToken); + + // Process the response + String content = await this.HandleResponse(httpResponse, cancellationToken); + + // call was successful so now deserialise the body to the response object + SaleTransactionResponseMessage responseMessage = JsonConvert.DeserializeObject(content); + + response = responseMessage.ResponseCode == "0000"; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception("Error posting sale transaction.", ex); + + throw exception; + } + + return response; + } + + public async Task PerformReconciliation(PerformReconciliationRequestModel model, CancellationToken cancellationToken) + { + Boolean response = false; + String requestUri = this.BuildRequestUrl("/api/transactions"); + + try + { + ReconciliationRequestMessage reconciliationRequest = new ReconciliationRequestMessage + { + ApplicationVersion = model.ApplicationVersion, + TransactionDateTime = model.TransactionDateTime, + DeviceIdentifier = model.DeviceIdentifier, + TransactionCount = model.TransactionCount, + TransactionValue = model.TransactionValue, + OperatorTotals = new List() + }; + foreach (OperatorTotalModel modelOperatorTotal in model.OperatorTotals) + { + reconciliationRequest.OperatorTotals.Add(new OperatorTotalRequest + { + OperatorIdentifier = modelOperatorTotal.OperatorIdentifier, + TransactionValue = modelOperatorTotal.TransactionValue, + ContractId = modelOperatorTotal.ContractId, + TransactionCount = modelOperatorTotal.TransactionCount + }); + } + + String requestSerialised = JsonConvert.SerializeObject(reconciliationRequest, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + StringContent httpContent = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + // Add the access token to the client headers + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, httpContent, cancellationToken); + + // Process the response + String content = await this.HandleResponse(httpResponse, cancellationToken); + + // call was successful so now deserialise the body to the response object + ReconciliationResponseMessage responseMessage = JsonConvert.DeserializeObject(content); + + response = responseMessage.ResponseCode == "0000"; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception("Error posting reconciliation transaction.", ex); + + throw exception; + } + + return response; + } + + public async Task PerformVoucherIssue(PerformVoucherIssueRequestModel model, + CancellationToken cancellationToken) + { + Boolean response = false; + String requestUri = this.BuildRequestUrl("/api/transactions"); + + try + { + SaleTransactionRequestMessage saleTransactionRequest = new SaleTransactionRequestMessage + { + ProductId = model.ProductId, + OperatorIdentifier = model.OperatorIdentifier, + ApplicationVersion = "1.0.5", //model.ApplicationVersion, + DeviceIdentifier = model.DeviceIdentifier, + ContractId = model.ContractId, + TransactionDateTime = model.TransactionDateTime, + CustomerEmailAddress = model.CustomerEmailAddress, + TransactionNumber = model.TransactionNumber + }; + + // Add the additional request data + saleTransactionRequest.AdditionalRequestMetaData = new Dictionary(); + saleTransactionRequest.AdditionalRequestMetaData.Add("Amount", model.VoucherAmount.ToString()); + saleTransactionRequest.AdditionalRequestMetaData.Add("RecipientEmail", model.RecipientEmailAddress); + saleTransactionRequest.AdditionalRequestMetaData.Add("RecipientMobile", model.RecipientMobileNumber); + + String requestSerialised = JsonConvert.SerializeObject(saleTransactionRequest, + new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.All + }); + + StringContent httpContent = new StringContent(requestSerialised, Encoding.UTF8, "application/json"); + + // Add the access token to the client headers + this.MemoryCacheService.TryGetValue("AccessToken", out TokenResponseModel accessToken); + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); + + // Make the Http Call here + HttpResponseMessage httpResponse = await this.HttpClient.PostAsync(requestUri, httpContent, cancellationToken); + + // Process the response + String content = await this.HandleResponse(httpResponse, cancellationToken); + + // call was successful so now deserialise the body to the response object + SaleTransactionResponseMessage responseMessage = JsonConvert.DeserializeObject(content); + + response = responseMessage.ResponseCode == "0000"; + } + catch (Exception ex) + { + // An exception has occurred, add some additional information to the message + Exception exception = new Exception("Error posting sale transaction.", ex); + + throw exception; + } + + return response; + } + } +} diff --git a/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj b/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj index ed90a3a4..7c2fc753 100644 --- a/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj +++ b/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj @@ -15,9 +15,14 @@ + + + + + diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/Admin/AdminPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/Admin/AdminPageViewModel.cs index 47eccba3..0d733305 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/Admin/AdminPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/Admin/AdminPageViewModel.cs @@ -7,6 +7,7 @@ using MvvmHelpers; using MvvmHelpers.Commands; using Requests; + using Services; using UIServices; public class AdminPageViewModel : BaseViewModel @@ -15,10 +16,8 @@ public class AdminPageViewModel : BaseViewModel private readonly INavigationService NavigationService; - private readonly IMemoryCache UserDetailsCache; - - private readonly IMemoryCache ConfigurationCache; - + private readonly IMemoryCacheService MemoryCacheService; + private readonly IDeviceService DeviceService; private readonly IApplicationInfoService ApplicationInfoService; @@ -26,13 +25,12 @@ public class AdminPageViewModel : BaseViewModel #region Constructors public AdminPageViewModel(IMediator mediator, INavigationService navigationService, - IMemoryCache userDetailsCache, IMemoryCache configurationCache, + IMemoryCacheService memoryCacheService, IDeviceService deviceService, IApplicationInfoService applicationInfoService) { this.Mediator = mediator; this.NavigationService = navigationService; - this.UserDetailsCache = userDetailsCache; - this.ConfigurationCache = configurationCache; + this.MemoryCacheService = memoryCacheService; this.DeviceService = deviceService; this.ApplicationInfoService = applicationInfoService; this.ReconciliationCommand = new AsyncCommand(this.ReconciliationCommandExecute); diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs index bc19fc5a..a80c970e 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs @@ -9,16 +9,15 @@ using MvvmHelpers; using MvvmHelpers.Commands; using Requests; + using Services; using UIServices; public class LoginPageViewModel : BaseViewModel { private readonly INavigationService NavigationService; - private readonly IMemoryCache UserDetailsCache; - - private readonly IMemoryCache ConfigurationCache; - + private readonly IMemoryCacheService MemoryCacheService; + private readonly IDeviceService DeviceService; private readonly IApplicationInfoService ApplicationInfoService; @@ -29,12 +28,11 @@ public class LoginPageViewModel : BaseViewModel #region Constructors - public LoginPageViewModel(IMediator mediator, INavigationService navigationService, IMemoryCache userDetailsCache, IMemoryCache configurationCache, + public LoginPageViewModel(IMediator mediator, INavigationService navigationService, IMemoryCacheService memoryCacheService, IDeviceService deviceService,IApplicationInfoService applicationInfoService) { this.NavigationService = navigationService; - this.UserDetailsCache = userDetailsCache; - this.ConfigurationCache = configurationCache; + this.MemoryCacheService = memoryCacheService; this.DeviceService = deviceService; this.ApplicationInfoService = applicationInfoService; this.LoginCommand = new AsyncCommand(this.LoginCommandExecute); @@ -68,11 +66,11 @@ public String Password private async Task LoginCommandExecute() { // TODO: this method needs refactored - - GetConfigurationRequest getConfigurationRequest = GetConfigurationRequest.Create(this.DeviceService.GetIdentifier()); + String deviceIdentifier = this.DeviceService.GetIdentifier(); + GetConfigurationRequest getConfigurationRequest = GetConfigurationRequest.Create(deviceIdentifier); Configuration configuration = await this.Mediator.Send(getConfigurationRequest); // Cache the config object - this.ConfigurationCache.Set("Configuration", configuration); + this.MemoryCacheService.Set("Configuration", configuration); LoginRequest loginRequest = LoginRequest.Create(this.UserName, this.Password); TokenResponseModel token = await this.Mediator.Send(loginRequest); @@ -86,7 +84,7 @@ private async Task LoginCommandExecute() // Logon Transaction LogonTransactionRequest logonTransactionRequest = LogonTransactionRequest.Create(DateTime.Now, "1", - this.DeviceService.GetIdentifier(), + deviceIdentifier, this.ApplicationInfoService.VersionString); PerformLogonResponseModel logonResponse = await this.Mediator.Send(logonTransactionRequest); @@ -96,22 +94,17 @@ private async Task LoginCommandExecute() } // Set the user information - this.UserDetailsCache.Set("EstateId", logonResponse.EstateId); - this.UserDetailsCache.Set("Merchant", logonResponse.MerchantId); + this.MemoryCacheService.Set("EstateId", logonResponse.EstateId); + this.MemoryCacheService.Set("MerchantId", logonResponse.MerchantId); // Get Contracts - // TODO: Cache the result, but will add this to a timer call to keep up to date... - GetContractProductsRequest getContractProductsRequest = GetContractProductsRequest.Create(token.AccessToken, - logonResponse.EstateId, - logonResponse.MerchantId, null); - List products = await this.Mediator.Send(getContractProductsRequest); - + GetContractProductsRequest getContractProductsRequest = GetContractProductsRequest.Create(); + await this.Mediator.Send(getContractProductsRequest); + // Get the merchant balance // TODO: Cache the result, but will add this to a timer call to keep up to date... - GetMerchantBalanceRequest getMerchantBalanceRequest = GetMerchantBalanceRequest.Create(token.AccessToken, - logonResponse.EstateId, - logonResponse.MerchantId); - Decimal merchantBalance = await this.Mediator.Send(getMerchantBalanceRequest); + GetMerchantBalanceRequest getMerchantBalanceRequest = GetMerchantBalanceRequest.Create(); + await this.Mediator.Send(getMerchantBalanceRequest); await this.NavigationService.GoToHome(); } @@ -147,9 +140,9 @@ private void CacheAccessToken(TokenResponseModel token) // Add eviction callback .RegisterPostEvictionCallback(callback:this.AccessTokenExpired); - this.UserDetailsCache.Set("AccessToken", token, cacheEntryOptions); + this.MemoryCacheService.Set("AccessToken", token, cacheEntryOptions); } - + #endregion } } \ No newline at end of file diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectOperatorPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectOperatorPageViewModel.cs index c2f55548..9c63cb04 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectOperatorPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectOperatorPageViewModel.cs @@ -43,7 +43,8 @@ public MobileTopupSelectOperatorPageViewModel(IMediator mediator, INavigationSer public async Task Initialise(CancellationToken cancellationToken) { - GetContractProductsRequest request = GetContractProductsRequest.Create("", Guid.Empty, Guid.Empty, ProductType.MobileTopup); + + GetContractProductsRequest request = GetContractProductsRequest.Create(ProductType.MobileTopup); List products = await this.Mediator.Send(request, cancellationToken); diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectProductPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectProductPageViewModel.cs index 7f29aab8..79468dc4 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectProductPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/MobileTopupSelectProductPageViewModel.cs @@ -52,7 +52,7 @@ public MobileTopupSelectProductPageViewModel(IMediator mediator, INavigationServ public async Task Initialise(CancellationToken cancellationToken) { - GetContractProductsRequest request = GetContractProductsRequest.Create("",Guid.Empty, Guid.Empty, ProductType.MobileTopup); + GetContractProductsRequest request = GetContractProductsRequest.Create(ProductType.MobileTopup); List products = await this.Mediator.Send(request, cancellationToken); diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectOperatorPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectOperatorPageViewModel.cs index 86fa3035..679233f9 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectOperatorPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectOperatorPageViewModel.cs @@ -43,7 +43,7 @@ public VoucherSelectOperatorPageViewModel(IMediator mediator, INavigationService public async Task Initialise(CancellationToken cancellationToken) { - GetContractProductsRequest request = GetContractProductsRequest.Create("", Guid.Empty, Guid.Empty, ProductType.Voucher); + GetContractProductsRequest request = GetContractProductsRequest.Create(ProductType.Voucher); List products = await this.Mediator.Send(request, cancellationToken); diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectProductPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectProductPageViewModel.cs index 6ae1fd02..c04c9f38 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectProductPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/Transactions/VoucherSelectProductPageViewModel.cs @@ -51,7 +51,7 @@ public VoucherSelectProductPageViewModel(IMediator mediator, INavigationService public async Task Initialise(CancellationToken cancellationToken) { - GetContractProductsRequest request = GetContractProductsRequest.Create("", Guid.Empty, Guid.Empty, ProductType.Voucher); + GetContractProductsRequest request = GetContractProductsRequest.Create(ProductType.Voucher); List products = await this.Mediator.Send(request, cancellationToken); diff --git a/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs b/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs index 59f6a3be..80f0f393 100644 --- a/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs +++ b/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs @@ -1,6 +1,8 @@ namespace TransactionMobile.Maui.Extensions { - + using System.Net.Security; + using System.Security.Cryptography.X509Certificates; + using Microsoft.Extensions.Caching.Memory; using BusinessLogic.Models; using BusinessLogic.RequestHandlers; using BusinessLogic.Requests; @@ -12,8 +14,16 @@ using BusinessLogic.ViewModels.Support; using BusinessLogic.ViewModels.Transactions; using Database; + using EstateManagement.Client; using MediatR; + using SecurityService.Client; using UIServices; + using TransactionMobile.Maui.Pages; + using TransactionMobile.Maui.Pages.Transactions; + using TransactionMobile.Maui.Pages.Transactions.MobileTopup; + using TransactionMobile.Maui.Pages.Transactions.Voucher; + using TransactionMobile.Maui.Pages.Transactions.Admin; + using TransactionMobile.Maui.Pages.Support; public static class MauiAppBuilderExtensions { @@ -28,13 +38,78 @@ public static MauiAppBuilder ConfigureDatabase(this MauiAppBuilder builder) return builder; } - + public static MauiAppBuilder ConfigureAppServices(this MauiAppBuilder builder) { - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + HttpClientHandler httpClientHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (message, + certificate2, + arg3, + arg4) => + { + return true; + } + }; + + HttpClient httpClient = new HttpClient(httpClientHandler); + builder.Services.AddSingleton(httpClient); + builder.Services.AddSingleton>( + new Func(configSetting => + { + + + if (configSetting == "ConfigServiceUrl") + { + return "https://5r8nmm.deta.dev"; + } + + IMemoryCacheService memoryCacheService = MauiProgram.Container.Services + .GetService(); + + Boolean configFound = memoryCacheService.TryGetValue("Configuration", out Configuration configuration); + + if (configFound && configuration != null) + { + if (configSetting == "SecurityService") + { + return configuration.SecurityServiceUri; + } + + if (configSetting == "TransactionProcessorACL") + { + return configuration.TransactionProcessorAclUri; + } + + if (configSetting == "EstateManagementApi") + { + return configuration.EstateManagementUri; + } + + if (configSetting == "EstateReportingApi") + { + return configuration.EstateReportingUri; + } + + return string.Empty; + } + + return string.Empty; + })); + + //builder.Services.AddSingleton(); + //builder.Services.AddSingleton(); + //builder.Services.AddSingleton(); + //builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } @@ -92,6 +167,30 @@ public static MauiAppBuilder ConfigureViewModels(this MauiAppBuilder builder) return builder; } + public static MauiAppBuilder ConfigurePages(this MauiAppBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + + builder.Services.AddTransient(); + + return builder; + } + #endregion } } \ No newline at end of file diff --git a/TransactionMobile.Maui/MauiProgram.cs b/TransactionMobile.Maui/MauiProgram.cs index 2631cfb8..f428364e 100644 --- a/TransactionMobile.Maui/MauiProgram.cs +++ b/TransactionMobile.Maui/MauiProgram.cs @@ -4,7 +4,7 @@ namespace TransactionMobile.Maui; using BusinessLogic.UIServices; using CommunityToolkit.Maui; -//using SQLitePCL; +using Microsoft.Extensions.Caching.Memory; using UIServices; public static class MauiProgram @@ -12,15 +12,27 @@ public static class MauiProgram public static MauiApp Container; public static MauiApp CreateMauiApp() { - //raw.SetProvider(new SQLite3Provider_sqlite3()); +#if ANDROID && DEBUG + Platforms.Services.DangerousAndroidMessageHandlerEmitter.Register(); + Platforms.Services.DangerousTrustProvider.Register(); +#endif + + //raw.SetProvider(new SQLite3Provider_sqlite3()); var builder = MauiApp.CreateBuilder(); - builder.UseMauiApp().ConfigureRequestHandlers().ConfigureViewModels().ConfigureAppServices().ConfigureUIServices().UseMauiCommunityToolkit().ConfigureDatabase() + builder.UseMauiApp() + .ConfigureRequestHandlers() + .ConfigurePages() + .ConfigureViewModels() + .ConfigureAppServices() + .ConfigureUIServices() + .UseMauiCommunityToolkit() + .ConfigureDatabase() .ConfigureFonts(fonts => - { - fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); - }) + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + }) .Services.AddTransient() - .AddMemoryCache(); + .AddMemoryCache(); Container = builder.Build(); diff --git a/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs b/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs index d0280268..b90be145 100644 --- a/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs +++ b/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs @@ -1,7 +1,5 @@ namespace TransactionMobile.Maui.Pages.AppHome; -using CommunityToolkit.Maui.Alerts; - public partial class HomePage : ContentPage { public HomePage() diff --git a/TransactionMobile.Maui/Platforms/Android/AndroidManifest.xml b/TransactionMobile.Maui/Platforms/Android/AndroidManifest.xml index 27c15461..5a333cd1 100644 --- a/TransactionMobile.Maui/Platforms/Android/AndroidManifest.xml +++ b/TransactionMobile.Maui/Platforms/Android/AndroidManifest.xml @@ -1,6 +1,6 @@  - + diff --git a/TransactionMobile.Maui/Platforms/Android/DeviceService.cs b/TransactionMobile.Maui/Platforms/Android/DeviceService.cs index 3d7ca047..43127f01 100644 --- a/TransactionMobile.Maui/Platforms/Android/DeviceService.cs +++ b/TransactionMobile.Maui/Platforms/Android/DeviceService.cs @@ -15,20 +15,10 @@ public static partial class DeviceInformationService { public static partial String Identifier() { - String id = Build.Serial; - //if (string.IsNullOrWhiteSpace(id) || id == Build.Unknown || id == "0") - //{ - // try - // { - // Application.Current.Con - // //Context context = Application.Context; - // //id = Settings.Secure.GetString(context.ContentResolver, Settings.Secure.AndroidId); - // } - // catch (Exception ex) - // { - // Log.Warn("DeviceInfo", "Unable to get id: " + ex); - // } - //} + var context = Android.App.Application.Context; + + string id = Android.Provider.Settings.Secure.GetString(context.ContentResolver, Settings.Secure.AndroidId); + return id; } diff --git a/TransactionMobile.Maui/Platforms/Android/SSLWorkarounds.cs b/TransactionMobile.Maui/Platforms/Android/SSLWorkarounds.cs new file mode 100644 index 00000000..587ab81e --- /dev/null +++ b/TransactionMobile.Maui/Platforms/Android/SSLWorkarounds.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.Platforms.Services +{ + using System.Reflection; + using System.Reflection.Emit; + using Java.Net; + using Java.Security; + using Java.Security.Cert; + using Javax.Net.Ssl; + using Xamarin.Android.Net; + + internal class DangerousTrustProvider : Provider + { + private const string TRUST_PROVIDER_ALG = "DangerousTrustAlgorithm"; + private const string TRUST_PROVIDER_ID = "DangerousTrustProvider"; + + public DangerousTrustProvider() : base(TRUST_PROVIDER_ID, 1, string.Empty) + { + var key = "TrustManagerFactory." + DangerousTrustManagerFactory.GetAlgorithm(); + var val = Java.Lang.Class.FromType(typeof(DangerousTrustManagerFactory)).Name; + Put(key, val); + } + + public static void Register() + { + Provider registered = Security.GetProvider(TRUST_PROVIDER_ID); + if (null == registered) + { + Security.InsertProviderAt(new DangerousTrustProvider(), 1); + Security.SetProperty("ssl.TrustManagerFactory.algorithm", TRUST_PROVIDER_ALG); + } + } + + public class DangerousTrustManager : X509ExtendedTrustManager + { + public override void CheckClientTrusted(X509Certificate[] chain, string authType, Socket socket) { } + + public override void CheckClientTrusted(X509Certificate[] chain, string authType, SSLEngine engine) { } + + public override void CheckClientTrusted(X509Certificate[] chain, string authType) { } + + public override void CheckServerTrusted(X509Certificate[] chain, string authType, Socket socket) { } + + public override void CheckServerTrusted(X509Certificate[] chain, string authType, SSLEngine engine) { } + + public override void CheckServerTrusted(X509Certificate[] chain, string authType) { } + + public override X509Certificate[] GetAcceptedIssuers() => Array.Empty(); + } + + public class DangerousTrustManagerFactory : TrustManagerFactorySpi + { + protected override void EngineInit(IManagerFactoryParameters mgrparams) { } + + protected override void EngineInit(KeyStore keystore) { } + + protected override ITrustManager[] EngineGetTrustManagers() => new ITrustManager[] { new DangerousTrustManager() }; + + public static string GetAlgorithm() => TRUST_PROVIDER_ALG; + } + } + + static class DangerousAndroidMessageHandlerEmitter + { + private static Assembly _emittedAssembly = null; + + public static void Register(string handlerTypeName = "DangerousAndroidMessageHandler", string assemblyName = "DangerousAndroidMessageHandler") + { + AppDomain.CurrentDomain.AssemblyResolve += (s, e) => + { + if (e.Name == assemblyName) + { + if (_emittedAssembly == null) + { + _emittedAssembly = Emit(handlerTypeName, assemblyName); + } + + return _emittedAssembly; + } + return null; + }; + } + + private static AssemblyBuilder Emit(string handlerTypeName, string assemblyName) + { + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.Run); + var module = assembly.DefineDynamicModule(assemblyName); + + DefineDangerousAndroidMessageHandler(module, handlerTypeName); + + return assembly; + } + + private static void DefineDangerousAndroidMessageHandler(ModuleBuilder module, string handlerTypeName) + { + var typeBuilder = module.DefineType(handlerTypeName, TypeAttributes.Public); + typeBuilder.SetParent(typeof(AndroidMessageHandler)); + typeBuilder.DefineDefaultConstructor(MethodAttributes.Public); + + var methodBuilder = typeBuilder.DefineMethod( + "GetSSLHostnameVerifier", + MethodAttributes.Public | MethodAttributes.Virtual, + typeof(IHostnameVerifier), + new[] { typeof(HttpsURLConnection) } + ); + + var generator = methodBuilder.GetILGenerator(); + generator.Emit(OpCodes.Call, typeof(DangerousHostNameVerifier).GetMethod("Create")); + generator.Emit(OpCodes.Ret); + + typeBuilder.CreateType(); + } + } + + public class DangerousHostNameVerifier : Java.Lang.Object, IHostnameVerifier + { + public bool Verify(string hostname, ISSLSession session) + { + return true; + } + + public static IHostnameVerifier Create() => new DangerousHostNameVerifier(); + } +} diff --git a/TransactionMobile.Maui/TransactionMobile.Maui.csproj b/TransactionMobile.Maui/TransactionMobile.Maui.csproj index 98394fcc..57c82f7d 100644 --- a/TransactionMobile.Maui/TransactionMobile.Maui.csproj +++ b/TransactionMobile.Maui/TransactionMobile.Maui.csproj @@ -8,15 +8,13 @@ true true enable - true + true Transaction Processing POS - com.transactionprocessing.pos - 1 @@ -58,6 +56,7 @@ + @@ -183,9 +182,21 @@ WinExe win10-x64 + false - + + DangerousAndroidMessageHandler, DangerousAndroidMessageHandler + + + + false + + + + true + + + true + diff --git a/TransactionMobile.Maui/UIServices/DeviceInformationService.cs b/TransactionMobile.Maui/UIServices/DeviceInformationService.cs index f1be6fdf..0ed0c3a8 100644 --- a/TransactionMobile.Maui/UIServices/DeviceInformationService.cs +++ b/TransactionMobile.Maui/UIServices/DeviceInformationService.cs @@ -3,12 +3,12 @@ public static partial class DeviceInformationService { #region Methods - - public static partial String Identifier(); - + public static partial String Model(); public static partial String Platform(); + public static partial String Identifier(); + #endregion } \ No newline at end of file diff --git a/TransactionMobile.Maui/UIServices/DeviceService.cs b/TransactionMobile.Maui/UIServices/DeviceService.cs index 9249ac3a..4956417d 100644 --- a/TransactionMobile.Maui/UIServices/DeviceService.cs +++ b/TransactionMobile.Maui/UIServices/DeviceService.cs @@ -5,6 +5,11 @@ public class DeviceService : IDeviceService { + public String GetIdentifier() + { + return DeviceInformationService.Identifier(); + } + public String GetModel() { return DeviceInformationService.Model(); @@ -14,9 +19,5 @@ public String GetPlatform() { return DeviceInformationService.Platform(); } - - public String GetIdentifier() - { - return DeviceInformationService.Identifier(); - } + } \ No newline at end of file diff --git a/TransactionMobile.Maui/UIServices/DialogService.cs b/TransactionMobile.Maui/UIServices/DialogService.cs index 1876bbc5..022f159b 100644 --- a/TransactionMobile.Maui/UIServices/DialogService.cs +++ b/TransactionMobile.Maui/UIServices/DialogService.cs @@ -6,8 +6,8 @@ namespace TransactionMobile.Maui.UIServices { - using CommunityToolkit.Maui.Alerts; - using CommunityToolkit.Maui.Core; + //using CommunityToolkit.Maui.Alerts; + //using CommunityToolkit.Maui.Core; //public interface IDialogService //{