From 44026c424f711c8754f04f8f2f8c57af4f197825 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 2 Jul 2026 11:14:01 +0100 Subject: [PATCH 1/3] Cache last login date and improve log upload handling Add methods to cache/retrieve last login date in ApplicationCache and update view models to use it. Update diagnostic log upload to return Result for better error handling. Apply minor code style improvements. --- .../Models/PerformVoucherIssueRequestModel.cs | 1 - .../RequestHandlers/SupportRequestHandler.cs | 8 +++++++- .../Services/ApplicationCache.cs | 13 +++++++++++++ .../Services/BalanceRefresher.cs | 3 ++- .../Services/ConfigurationService.cs | 8 ++++---- .../TrainingConfigurationService.cs | 3 ++- .../ViewModels/LoginPageViewModel.cs | 3 +++ .../ViewModels/MyAccount/MyAccountPageViewModel.cs | 2 +- .../VoucherPerformIssuePageViewModel.cs | 1 - TransactionProcessor.Mobile/MauiProgram.cs | 2 +- 10 files changed, 33 insertions(+), 11 deletions(-) diff --git a/TransactionProcessor.Mobile.BusinessLogic/Models/PerformVoucherIssueRequestModel.cs b/TransactionProcessor.Mobile.BusinessLogic/Models/PerformVoucherIssueRequestModel.cs index ae7e9abc1..633e05db0 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Models/PerformVoucherIssueRequestModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Models/PerformVoucherIssueRequestModel.cs @@ -5,7 +5,6 @@ namespace TransactionProcessor.Mobile.BusinessLogic.Models; [ExcludeFromCodeCoverage] public class PerformVoucherIssueRequestModel { - // TODO: should we have a base transaction request model ? #region Properties diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs index 76b01c214..8f53b4322 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/SupportRequestHandler.cs @@ -49,10 +49,16 @@ public async Task Handle(SupportCommands.UploadLogsCommand request, Canc })); - await configurationService.PostDiagnosticLogs(request.DeviceIdentifier, logMessageModels, CancellationToken.None); + Result result = await configurationService.PostDiagnosticLogs(request.DeviceIdentifier, logMessageModels, CancellationToken.None); + + if (result.IsFailed) { + // We have had a failure posting the logs so we will stop trying to upload any more logs + return result; + } // Clear the logs that have been uploaded await this.DatabaseContext.RemoveUploadedMessages(logEntries); + } return Result.Success(); diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs index d1389f1b1..483926d50 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ApplicationCache.cs @@ -42,6 +42,10 @@ public interface IApplicationCache Decimal GetMerchantBalance(); void SetMerchantBalance(Decimal value, MemoryCacheEntryOptions options = default); + + DateTime GetLastLoginDate(); + + void SetLastLoginDate(DateTime value, MemoryCacheEntryOptions options = default); } [ExcludeFromCodeCoverage] @@ -148,6 +152,15 @@ public void SetMerchantBalance(Decimal value, this.Set("MerchantBalance", value, options); } + public DateTime GetLastLoginDate() { + return this.TryGetValue("LastLoginDate"); + } + + public void SetLastLoginDate(DateTime value, + MemoryCacheEntryOptions options = default) { + this.Set("LastLoginDate", value, options); + } + public void SetUseTrainingMode(Boolean value, MemoryCacheEntryOptions options = default) { diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs index b33ec15db..7f21f15c5 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/BalanceRefresher.cs @@ -38,7 +38,7 @@ public void StopRefreshing() private async Task RefreshLoopAsync(CancellationToken token) { - using PeriodicTimer timer = new PeriodicTimer(TimeSpan.FromSeconds(30)); + using PeriodicTimer timer = new(TimeSpan.FromSeconds(30)); try { @@ -52,6 +52,7 @@ private async Task RefreshLoopAsync(CancellationToken token) } catch (OperationCanceledException) { + // Nothing here but the task was cancelled, which is expected when stopping the refresher. } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs index 7500277ef..b8f8faf8d 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ConfigurationService.cs @@ -12,7 +12,7 @@ public interface IConfigurationService Task> GetConfiguration(String deviceIdentifier, CancellationToken cancellationToken); - Task PostDiagnosticLogs(String deviceIdentifier, + Task PostDiagnosticLogs(String deviceIdentifier, List logMessages, CancellationToken cancellationToken); } @@ -92,7 +92,7 @@ public async Task> GetConfiguration(String deviceIdentifie } } - public async Task PostDiagnosticLogs(String deviceIdentifier, + public async Task PostDiagnosticLogs(String deviceIdentifier, List logMessages, CancellationToken cancellationToken) { @@ -105,8 +105,8 @@ public async Task PostDiagnosticLogs(String deviceIdentifier, }; StringContent content = new(StringSerialiser.Serialise(container), Encoding.UTF8, "application/json"); - Result? result = await this.Post(requestUri, content, cancellationToken); + Result result = await this.Post(requestUri, content, cancellationToken); - // TODO: return the result to the caller so that we can retry if it fails (and also log any errors that occur here) + return result; } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/TrainingModeServices/TrainingConfigurationService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/TrainingModeServices/TrainingConfigurationService.cs index 2bec7e770..cb1756912 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/TrainingModeServices/TrainingConfigurationService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/TrainingModeServices/TrainingConfigurationService.cs @@ -24,9 +24,10 @@ public async Task> GetConfiguration(String deviceIdentifie }); } - public async Task PostDiagnosticLogs(String deviceIdentifier, + public async Task PostDiagnosticLogs(String deviceIdentifier, List logMessages, CancellationToken cancellationToken) { // Do nothing + return Result.Success(); } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs index 923fe787a..c8cffbb89 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/LoginPageViewModel.cs @@ -254,6 +254,9 @@ private async Task Logon(){ await this.WriteTimingTrace(sw, "After SetIsLoggedIn"); await this.NavigationService.GoToHome(); + + // Cache the last login date + this.ApplicationCache.SetLastLoginDate(DateTime.Now); } catch(ApplicationException aex) { Logger.LogError("Error during logon", aex); diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs index 63a9e76fb..b11332cbb 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/MyAccount/MyAccountPageViewModel.cs @@ -83,7 +83,7 @@ public async Task Initialise(CancellationToken cancellationToken) { this.MerchantName = merchantDetailsResult.Data.MerchantName; - this.LastLogin = DateTime.Now; // TODO: might cache this in the application + this.LastLogin = this.ApplicationCache.GetLastLoginDate(); this.IsDarkThemeEnabled = await this.ApplicationThemeService.GetDarkThemeEnabled(); } diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Transactions/VoucherPerformIssuePageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Transactions/VoucherPerformIssuePageViewModel.cs index 8f8c443c3..d93fef4fc 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Transactions/VoucherPerformIssuePageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Transactions/VoucherPerformIssuePageViewModel.cs @@ -112,7 +112,6 @@ private async Task RecipientEmailAddressEntryCompleted() private async Task IssueVoucher() { Logger.LogInformation("IssueVoucher called"); - // TODO: Create Command and Send TransactionCommands.PerformVoucherIssueCommand command = new TransactionCommands.PerformVoucherIssueCommand(DateTime.Now, this.ProductDetails.ContractId, this.ProductDetails.ProductId, diff --git a/TransactionProcessor.Mobile/MauiProgram.cs b/TransactionProcessor.Mobile/MauiProgram.cs index 7a8288eb7..7e7391dd0 100644 --- a/TransactionProcessor.Mobile/MauiProgram.cs +++ b/TransactionProcessor.Mobile/MauiProgram.cs @@ -31,7 +31,7 @@ public static MauiApp CreateMauiApp() }).Services.AddTransient() .AddMemoryCache(); - builder.Logging.SetMinimumLevel(LogLevel.Trace);; + builder.Logging.SetMinimumLevel(LogLevel.Trace); Container = builder.Build(); From e7b9e9a32dd4948c600f62a08d466ede262753f9 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 2 Jul 2026 12:19:49 +0100 Subject: [PATCH 2/3] fix failed unit tests --- .../ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs index 3d2c123be..db94dd3fd 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/MyAccount/MyAccountPageViewModelTests.cs @@ -49,7 +49,7 @@ public MyAccountPageViewModelTests() { public async Task MyAccountPageViewModel_Initialise_IsInitialised() { this.ApplicationThemeService.Setup(s => s.GetDarkThemeEnabled()).ReturnsAsync(true); this.Mediator.Setup(m => m.Send(It.IsAny(), It.IsAny())).ReturnsAsync(Result.Success( TestData.MerchantDetailsModel)); - + this.ApplicationCache.Setup(a => a.GetLastLoginDate()).Returns(DateTime.Now); await this.ViewModel.Initialise(CancellationToken.None); this.ViewModel.MerchantName.ShouldBe(TestData.MerchantDetailsModel.MerchantName); From 940476d8027cbc44ac88413be5cb68c2981b545d Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Thu, 2 Jul 2026 13:49:36 +0100 Subject: [PATCH 3/3] :| --- .../RequestHandlerTests/SupportRequestHandlerTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs index d9972eb84..c7696e36a 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/SupportRequestHandlerTests.cs @@ -40,6 +40,7 @@ public async Task SupportRequestHandlerTests_UploadLogsRequest_NoLogs_Handle_IsH public async Task SupportRequestHandlerTests_UploadLogsRequest_LogsToUpload_Only10Messages_Handle_IsHandled() { Mock configurationService = new Mock(); + configurationService.Setup(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), It.IsAny())).ReturnsAsync(Result.Success()); Func configurationServiceResolver = new Func((param) => { return configurationService.Object; @@ -76,6 +77,7 @@ public async Task SupportRequestHandlerTests_UploadLogsRequest_LogsToUpload_Only public async Task SupportRequestHandlerTests_UploadLogsRequest_LogsToUpload_15Messages_Handle_IsHandled() { Mock configurationService = new Mock(); + configurationService.Setup(c => c.PostDiagnosticLogs(It.IsAny(), It.IsAny>(), It.IsAny())).ReturnsAsync(Result.Success()); Func configurationServiceResolver = new Func((param) => { return configurationService.Object;