diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs index 1e8fa168..7638291c 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/RequestHandlerTests/ReportRequestHandlerTests.cs @@ -96,4 +96,93 @@ public async Task GetTransactionMixSummaryQuery_ReturnsRequestedBreakdown() It.IsAny()), Times.Once); } + + [Fact] + public async Task GetRecentActivityReceiptReportQuery_ReturnsMockedResultsForOneDate() + { + Mock reportsService = new(); + Mock applicationCache = new(); + MerchantDetailsModel merchantDetails = new() + { + MerchantReportingId = 12345 + }; + + DateTime reportDate = new(2026, 7, 6); + + applicationCache.Setup(a => a.GetMerchantDetails()).Returns(merchantDetails); + reportsService.Setup(r => r.GetRecentActivityReceiptReport( + merchantDetails.MerchantReportingId, + reportDate, + "TXN-10001", + 1, + 5, + It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, "TXN-10001"))); + + ReportRequestHandler handler = new(reportsService.Object, applicationCache.Object); + + Result result = await handler.Handle( + new ReportQueries.GetRecentActivityReceiptReportQuery( + ReportDate: reportDate, + SearchText: "TXN-10001", + PageNumber: 1, + PageSize: 5), + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ReportDate.ShouldBe(reportDate); + result.Data.Items.ShouldNotBeEmpty(); + result.Data.Items.All(item => item.TransactionDateTime.Date == reportDate).ShouldBeTrue(); + reportsService.Verify(r => r.GetRecentActivityReceiptReport( + merchantDetails.MerchantReportingId, + reportDate, + "TXN-10001", + 1, + 5, + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task GetRecentActivityReceiptReportQuery_PassesPagingToService() + { + Mock reportsService = new(); + Mock applicationCache = new(); + MerchantDetailsModel merchantDetails = new() + { + MerchantReportingId = 12345 + }; + + DateTime reportDate = new(2026, 7, 6); + + applicationCache.Setup(a => a.GetMerchantDetails()).Returns(merchantDetails); + reportsService.Setup(r => r.GetRecentActivityReceiptReport( + merchantDetails.MerchantReportingId, + reportDate, + null, + 2, + 5, + It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, null))); + + ReportRequestHandler handler = new(reportsService.Object, applicationCache.Object); + + Result result = await handler.Handle( + new ReportQueries.GetRecentActivityReceiptReportQuery( + ReportDate: reportDate, + SearchText: null, + PageNumber: 2, + PageSize: 5), + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + reportsService.Verify(r => r.GetRecentActivityReceiptReport( + merchantDetails.MerchantReportingId, + reportDate, + null, + 2, + 5, + It.IsAny()), + Times.Once); + } } diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ReportsServiceTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ReportsServiceTests.cs index a51c1009..dfdcd433 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ReportsServiceTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ServicesTests/ReportsServiceTests.cs @@ -99,4 +99,176 @@ public async Task GetTransactionMixSummary_SendsRequestAndMapsResponse() requestPayload.ShouldContain("\"merchant_reporting_id\": 12345"); requestPayload.ShouldContain("\"top_n\": 5"); } + + [Fact] + public async Task GetRecentActivityReceiptReport_SendsRequestAndMapsResponse() + { + RecentActivityReceiptSearchResponseDto response = new() + { + ReportDate = new DateTime(2026, 7, 6), + PageNumber = 1, + PageSize = 5, + TotalCount = 2, + Items = + [ + new RecentActivityReceiptSearchItemDto + { + Reference = "TXN-10002", + TransactionType = "Bill Payment", + Product = "Bill Pay (Post)", + Operator = "PataPawa PostPay", + Status = "Success", + Amount = 250.00m, + TransactionDateTime = new DateTime(2026, 7, 6, 10, 15, 0), + ReceiptReference = "RCPT-10002" + }, + new RecentActivityReceiptSearchItemDto + { + Reference = "TXN-10001", + TransactionType = "Mobile Topup", + Product = "Custom", + Operator = "Safaricom", + Status = "Success", + Amount = 100.00m, + TransactionDateTime = new DateTime(2026, 7, 6, 9, 30, 0), + ReceiptReference = "RCPT-10001" + } + ] + }; + + String requestPayload = string.Empty; + this.MockHttpMessageHandler.When(HttpMethod.Post, "http://localhost/api/reporting/recentactivityreceiptsearch") + .Respond(req => + { + requestPayload = req.Content!.ReadAsStringAsync().GetAwaiter().GetResult(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(StringSerialiser.Serialise(response), Encoding.UTF8, "application/json") + }; + }); + + Result result = await this.ReportsService.GetRecentActivityReceiptReport(12345, + new DateTime(2026, 7, 6), + "TXN-10001", + 1, + 5, + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ReportDate.ShouldBe(new DateTime(2026, 7, 6)); + result.Data.Items.Count.ShouldBe(2); + result.Data.Items[0].Reference.ShouldBe("TXN-10002"); + result.Data.Items[1].Reference.ShouldBe("TXN-10001"); + result.Data.PageNumber.ShouldBe(1); + result.Data.PageSize.ShouldBe(5); + result.Data.TotalCount.ShouldBe(2); + requestPayload.ShouldContain("\"application_version\": \"1.2.3\""); + requestPayload.ShouldContain("\"merchant_reporting_id\": 12345"); + requestPayload.ShouldContain("\"report_date\""); + requestPayload.ShouldContain("2026-07-06"); + requestPayload.ShouldContain("\"search_text\": \"TXN-10001\""); + } + + [Fact] + public async Task GetRecentActivityReceiptReport_ReturnsSecondPage() + { + RecentActivityReceiptSearchResponseDto response = new() + { + ReportDate = new DateTime(2026, 7, 6), + PageNumber = 2, + PageSize = 5, + TotalCount = 10, + Items = + [ + new RecentActivityReceiptSearchItemDto + { + Reference = "TXN-10005", + TransactionType = "Bill Payment", + Product = "Prepaid Power", + Operator = "KPLC", + Status = "Failed", + Amount = 75.00m, + TransactionDateTime = new DateTime(2026, 7, 6, 13, 5, 0), + ReceiptReference = "RCPT-10005" + } + ] + }; + + this.MockHttpMessageHandler.When(HttpMethod.Post, "http://localhost/api/reporting/recentactivityreceiptsearch") + .Respond(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(StringSerialiser.Serialise(response), Encoding.UTF8, "application/json") + }); + + Result result = await this.ReportsService.GetRecentActivityReceiptReport(12345, + new DateTime(2026, 7, 6), + null, + 2, + 5, + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.PageNumber.ShouldBe(2); + result.Data.PageSize.ShouldBe(5); + result.Data.TotalCount.ShouldBe(10); + result.Data.Items.Count.ShouldBe(1); + } + + [Fact] + public async Task ResendRecentActivityReceipt_SendsRequestAndMapsSuccessResponse() + { + RecentActivityReceiptResendResponseDto response = new() + { + Success = true, + Message = "Receipt resend requested.", + Reference = "TXN-10001", + ReceiptReference = "RCPT-10001", + TransactionReference = "TXN-10001" + }; + + String requestPayload = string.Empty; + this.MockHttpMessageHandler.When(HttpMethod.Post, "http://localhost/api/transactions/resendreceipt") + .Respond(req => + { + requestPayload = req.Content!.ReadAsStringAsync().GetAwaiter().GetResult(); + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(StringSerialiser.Serialise(response), Encoding.UTF8, "application/json") + }; + }); + + Result result = await this.ReportsService.ResendRecentActivityReceipt("TXN-10001", + "customer@example.com", + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.Success.ShouldBeTrue(); + result.Data.Message.ShouldBe("Receipt resend requested."); + result.Data.Reference.ShouldBe("TXN-10001"); + requestPayload.ShouldContain("\"reference\": \"TXN-10001\""); + requestPayload.ShouldContain("\"recipient_email_address\": \"customer@example.com\""); + } + + [Fact] + public async Task ResendRecentActivityReceipt_ReturnsFailureWhenApiRejectsRequest() + { + RecentActivityReceiptResendResponseDto response = new() + { + Success = false, + Message = "Recipient email address is not valid." + }; + + this.MockHttpMessageHandler.When(HttpMethod.Post, "http://localhost/api/transactions/resendreceipt") + .Respond(_ => new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(StringSerialiser.Serialise(response), Encoding.UTF8, "application/json") + }); + + Result result = await this.ReportsService.ResendRecentActivityReceipt("TXN-10001", + "invalid@example", + CancellationToken.None); + + result.IsFailed.ShouldBeTrue(); + result.Message.ShouldBe("Recipient email address is not valid."); + } } diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReceiptDetailPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReceiptDetailPageViewModelTests.cs new file mode 100644 index 00000000..acdbfe82 --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReceiptDetailPageViewModelTests.cs @@ -0,0 +1,156 @@ +using Moq; +using Shouldly; +using SimpleResults; +using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Services; +using TransactionProcessor.Mobile.BusinessLogic.UIServices; +using TransactionProcessor.Mobile.BusinessLogic.ViewModels.Reports; + +namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ViewModelTests.Reports; + +public class RecentActivityReceiptDetailPageViewModelTests +{ + private readonly Mock NavigationService; + private readonly Mock ApplicationCache; + private readonly Mock DialogService; + private readonly Mock DeviceService; + private readonly Mock NavigationParameterService; + private readonly Mock ReportsService; + private readonly RecentActivityReceiptDetailPageViewModel ViewModel; + + public RecentActivityReceiptDetailPageViewModelTests() + { + this.NavigationService = new Mock(); + this.ApplicationCache = new Mock(); + this.DialogService = new Mock(); + this.DeviceService = new Mock(); + this.NavigationParameterService = new Mock(); + this.ReportsService = new Mock(); + this.ViewModel = new RecentActivityReceiptDetailPageViewModel(this.NavigationService.Object, + this.ApplicationCache.Object, + this.DialogService.Object, + this.DeviceService.Object, + this.NavigationParameterService.Object, + this.ReportsService.Object); + } + + [Fact] + public async Task Initialise_LoadsSelectedItemFromNavigationParameters() + { + RecentActivityReceiptItemModel item = new("TXN-10001", + "Mobile Topup", + "Custom", + "Safaricom", + "Success", + 100.00m, + new DateTime(2026, 7, 6, 9, 30, 0), + "RCPT-10001"); + + this.NavigationParameterService.Setup(p => p.GetParameters()).Returns(new Dictionary + { + { nameof(RecentActivityReceiptItemModel), item } + }); + + await this.ViewModel.Initialise(CancellationToken.None); + + this.ViewModel.Reference.ShouldBe("TXN-10001"); + this.ViewModel.ReceiptReference.ShouldBe("RCPT-10001"); + this.ViewModel.HasReceipt.ShouldBeTrue(); + } + + [Fact] + public async Task ResendReceiptCommand_SendsReceiptAndShowsSuccessMessage() + { + RecentActivityReceiptItemModel item = new("TXN-10001", + "Mobile Topup", + "Custom", + "Safaricom", + "Success", + 100.00m, + new DateTime(2026, 7, 6, 9, 30, 0), + "RCPT-10001"); + + this.NavigationParameterService.Setup(p => p.GetParameters()).Returns(new Dictionary + { + { nameof(RecentActivityReceiptItemModel), item } + }); + + await this.ViewModel.Initialise(CancellationToken.None); + + this.ViewModel.EmailAddress = "customer@example.com"; + this.ReportsService.Setup(r => r.ResendRecentActivityReceipt("TXN-10001", + "customer@example.com", + It.IsAny())) + .ReturnsAsync(Result.Success(new RecentActivityReceiptResendResultModel + { + Success = true, + Message = "Receipt resend requested.", + Reference = "TXN-10001" + })); + + await this.ViewModel.ResendReceiptCommand.ExecuteAsync(null); + + this.ReportsService.Verify(r => r.ResendRecentActivityReceipt("TXN-10001", + "customer@example.com", + It.IsAny()), + Times.Once); + this.DialogService.Verify(d => d.ShowSuccessToast("Receipt resend requested. Sent to customer@example.com.", null, "OK", null, It.IsAny()), Times.Once); + this.ViewModel.EmailAddress.ShouldBe(string.Empty); + } + + [Fact] + public async Task ResendReceiptCommand_ShowsWarningForInvalidEmail() + { + RecentActivityReceiptItemModel item = new("TXN-10001", + "Mobile Topup", + "Custom", + "Safaricom", + "Success", + 100.00m, + new DateTime(2026, 7, 6, 9, 30, 0), + "RCPT-10001"); + + this.NavigationParameterService.Setup(p => p.GetParameters()).Returns(new Dictionary + { + { nameof(RecentActivityReceiptItemModel), item } + }); + + await this.ViewModel.Initialise(CancellationToken.None); + + this.ViewModel.EmailAddress = "invalid-email"; + + await this.ViewModel.ResendReceiptCommand.ExecuteAsync(null); + + this.DialogService.Verify(d => d.ShowWarningToast("Enter a valid email address before resending the receipt.", null, "OK", null, It.IsAny()), Times.Once); + } + + [Fact] + public async Task ResendReceiptCommand_ShowsWarningWhenApiFails() + { + RecentActivityReceiptItemModel item = new("TXN-10001", + "Mobile Topup", + "Custom", + "Safaricom", + "Success", + 100.00m, + new DateTime(2026, 7, 6, 9, 30, 0), + "RCPT-10001"); + + this.NavigationParameterService.Setup(p => p.GetParameters()).Returns(new Dictionary + { + { nameof(RecentActivityReceiptItemModel), item } + }); + + await this.ViewModel.Initialise(CancellationToken.None); + + this.ViewModel.EmailAddress = "customer@example.com"; + this.ReportsService.Setup(r => r.ResendRecentActivityReceipt("TXN-10001", + "customer@example.com", + It.IsAny())) + .ReturnsAsync(Result.Failure("Receipt resend failed.")); + + await this.ViewModel.ResendReceiptCommand.ExecuteAsync(null); + + this.DialogService.Verify(d => d.ShowWarningToast("Receipt resend failed.", null, "OK", null, It.IsAny()), Times.Once); + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReportPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReportPageViewModelTests.cs new file mode 100644 index 00000000..ae6b634c --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/RecentActivityReportPageViewModelTests.cs @@ -0,0 +1,95 @@ +using MediatR; +using Moq; +using Shouldly; +using SimpleResults; +using TransactionProcessor.Mobile.BusinessLogic.Common; +using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Requests; +using TransactionProcessor.Mobile.BusinessLogic.Services; +using TransactionProcessor.Mobile.BusinessLogic.UIServices; +using TransactionProcessor.Mobile.BusinessLogic.ViewModels.Reports; + +namespace TransactionProcessor.Mobile.BusinessLogic.Tests.ViewModelTests.Reports; + +public class RecentActivityReportPageViewModelTests +{ + private readonly Mock Mediator; + private readonly Mock NavigationService; + private readonly Mock ApplicationCache; + private readonly Mock DialogService; + private readonly Mock DeviceService; + private readonly Mock NavigationParameterService; + private readonly RecentActivityReportPageViewModel ViewModel; + + public RecentActivityReportPageViewModelTests() + { + this.Mediator = new Mock(); + this.NavigationService = new Mock(); + this.ApplicationCache = new Mock(); + this.DialogService = new Mock(); + this.DeviceService = new Mock(); + this.NavigationParameterService = new Mock(); + + this.ViewModel = new RecentActivityReportPageViewModel(this.Mediator.Object, + this.NavigationService.Object, + this.ApplicationCache.Object, + this.DialogService.Object, + this.DeviceService.Object, + this.NavigationParameterService.Object); + } + + [Fact] + public async Task Initialise_LoadsMockedResultsForToday() + { + DateTime reportDate = new(2026, 7, 6); + this.Mediator + .Setup(m => m.Send(It.Is(q => q.ReportDate == reportDate && q.SearchText == null && q.PageNumber == 1 && q.PageSize == 5), It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, null))); + + this.ViewModel.SelectedDate = reportDate; + + await this.ViewModel.Initialise(CancellationToken.None); + + this.ViewModel.SelectedDate.ShouldBe(reportDate); + this.ViewModel.Items.Count.ShouldBeGreaterThan(0); + this.ViewModel.IsLoading.ShouldBeFalse(); + } + + [Fact] + public async Task SearchCommand_UsesSelectedDateAndSearchText() + { + DateTime reportDate = new(2026, 7, 6); + this.Mediator + .Setup(m => m.Send(It.Is(q => q.ReportDate == reportDate && q.SearchText == "TXN-10001" && q.PageNumber == 1 && q.PageSize == 5), It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, "TXN-10001"))); + + this.ViewModel.SelectedDate = reportDate; + this.ViewModel.SearchText = "TXN-10001"; + + await this.ViewModel.SearchCommand.ExecuteAsync(null); + + this.ViewModel.Items.ShouldContain(item => item.Reference == "TXN-10001"); + this.Mediator.VerifyAll(); + } + + [Fact] + public async Task NextPageCommand_RequestsTheNextPage() + { + DateTime reportDate = new(2026, 7, 6); + this.Mediator + .Setup(m => m.Send(It.Is(q => q.ReportDate == reportDate && q.PageNumber == 1 && q.PageSize == 5), It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, null, 1, 5))); + this.Mediator + .Setup(m => m.Send(It.Is(q => q.ReportDate == reportDate && q.PageNumber == 2 && q.PageSize == 5), It.IsAny())) + .ReturnsAsync(Result.Success(RecentActivityReceiptReportModel.CreateMock(reportDate, null, 2, 5))); + + this.ViewModel.SelectedDate = reportDate; + + await this.ViewModel.Initialise(CancellationToken.None); + await this.ViewModel.NextPageCommand.ExecuteAsync(null); + + this.ViewModel.PageNumber.ShouldBe(2); + this.ViewModel.Items.ShouldAllBe(item => item.TransactionDateTime.Date == reportDate); + this.Mediator.Verify(m => m.Send(It.Is(q => q.PageNumber == 2 && q.PageSize == 5), It.IsAny()), Times.Once); + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/ReportsPageViewModelTests.cs b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/ReportsPageViewModelTests.cs index 5e8569f6..d8e0c933 100644 --- a/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/ReportsPageViewModelTests.cs +++ b/TransactionProcessor.Mobile.BusinessLogic.Tests/ViewModelTests/Reports/ReportsPageViewModelTests.cs @@ -66,7 +66,7 @@ public async Task ReportsPageViewModel_OptionSelectedCommand_Execute_IsExecuted( public async Task ReportsPageViewModel_Initialise_IsInitialised() { await this.ViewModel.Initialise(CancellationToken.None); - this.ViewModel.ReportsMenuOptions.Count.ShouldBe(2); + this.ViewModel.ReportsMenuOptions.Count.ShouldBe(3); } [Fact] @@ -83,6 +83,20 @@ public async Task ReportsPageViewModel_TransactionMixCommand_Execute_IsExecuted( this.NavigationService.Verify(v => v.GoToTransactionMixSummaryPage(), Times.Once); } + [Fact] + public async Task ReportsPageViewModel_RecentActivityCommand_Execute_IsExecuted() + { + ItemSelected itemSelected = new ItemSelected + { + SelectedItem = new ListViewItem { Title = "Recent Activity and Receipt Report" }, + SelectedItemIndex = 2, + }; + + this.ViewModel.OptionSelectedCommand.Execute(itemSelected); + + this.NavigationService.Verify(v => v.GoToRecentActivityReportPage(), Times.Once); + } + [Fact] public async Task ReportsPageViewModel_BackButtonCommand_HomePageIsShown() { diff --git a/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptReportModel.cs b/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptReportModel.cs new file mode 100644 index 00000000..324d7fe4 --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptReportModel.cs @@ -0,0 +1,82 @@ +namespace TransactionProcessor.Mobile.BusinessLogic.Models; + +public sealed record RecentActivityReceiptReportModel +{ + public DateTime ReportDate { get; set; } + + public string? SearchText { get; set; } + + public int PageNumber { get; set; } + + public int PageSize { get; set; } + + public int TotalCount { get; set; } + + public IReadOnlyList Items { get; set; } = []; + + public int TotalPages => this.PageSize <= 0 + ? 0 + : (int)Math.Ceiling(this.TotalCount / (decimal)this.PageSize); + + public static RecentActivityReceiptReportModel CreateMock(DateTime reportDate, + string? searchText, + int pageNumber = 1, + int pageSize = 5) + { + List items = + [ + new("TXN-10001", "Mobile Topup", "Custom", "Safaricom", "Success", 100.00m, reportDate.Date.AddHours(9).AddMinutes(30), "RCPT-10001"), + new("TXN-10002", "Bill Payment", "Bill Pay (Post)", "PataPawa PostPay", "Success", 250.00m, reportDate.Date.AddHours(10).AddMinutes(15), "RCPT-10002"), + new("TXN-10003", "Voucher Issue", "10 KES", "Voucher", "Failed", 0.00m, reportDate.Date.AddHours(11).AddMinutes(5), "RCPT-10003"), + new("TXN-10004", "Mobile Topup", "Airtime", "Airtel", "Success", 50.00m, reportDate.Date.AddHours(12).AddMinutes(20), "RCPT-10004"), + new("TXN-10005", "Bill Payment", "Prepaid Power", "KPLC", "Failed", 75.00m, reportDate.Date.AddHours(13).AddMinutes(5), "RCPT-10005"), + new("TXN-10006", "Voucher Issue", "20 KES", "Voucher", "Success", 20.00m, reportDate.Date.AddHours(14).AddMinutes(10), "RCPT-10006"), + new("TXN-10007", "Mobile Topup", "Custom", "Safaricom", "Success", 500.00m, reportDate.Date.AddHours(15).AddMinutes(55), "RCPT-10007"), + ]; + + if (string.IsNullOrWhiteSpace(searchText) == false) + { + string term = searchText.Trim(); + items = items.Where(item => item.Matches(term)).ToList(); + } + + items = items.OrderByDescending(item => item.TransactionDateTime).ToList(); + + int normalizedPageSize = pageSize > 0 ? pageSize : 5; + int normalizedPageNumber = pageNumber > 0 ? pageNumber : 1; + int totalCount = items.Count; + + return new RecentActivityReceiptReportModel + { + ReportDate = reportDate.Date, + SearchText = searchText, + PageNumber = normalizedPageNumber, + PageSize = normalizedPageSize, + TotalCount = totalCount, + Items = items.Skip((normalizedPageNumber - 1) * normalizedPageSize) + .Take(normalizedPageSize) + .ToList(), + }; + } +} + +public sealed record RecentActivityReceiptItemModel(string Reference, + string TransactionType, + string Product, + string Operator, + string Status, + decimal Amount, + DateTime TransactionDateTime, + string ReceiptReference) +{ + public bool Matches(string searchText) + { + return this.Reference.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.TransactionType.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.Product.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.Operator.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.Status.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.ReceiptReference.Contains(searchText, StringComparison.OrdinalIgnoreCase) + || this.Amount.ToString("0.##").Contains(searchText, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptResendResultModel.cs b/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptResendResultModel.cs new file mode 100644 index 00000000..c2240c1a --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/Models/RecentActivityReceiptResendResultModel.cs @@ -0,0 +1,14 @@ +namespace TransactionProcessor.Mobile.BusinessLogic.Models; + +public sealed record RecentActivityReceiptResendResultModel +{ + public bool Success { get; set; } + + public string? Message { get; set; } + + public string? Reference { get; set; } + + public string? ReceiptReference { get; set; } + + public string? TransactionReference { get; set; } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs index da78b505..828cec08 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/RequestHandlers/ReportRequestHandler.cs @@ -7,7 +7,8 @@ namespace TransactionProcessor.Mobile.BusinessLogic.RequestHandlers; public sealed class ReportRequestHandler : IRequestHandler>, - IRequestHandler> { + IRequestHandler>, + IRequestHandler> { private readonly IReportsService ReportsService; private readonly IApplicationCache ApplicationCache; @@ -56,4 +57,21 @@ public async Task> Handle(ReportQueries.GetTr request.TopN, cancellationToken); } + + public async Task> Handle(ReportQueries.GetRecentActivityReceiptReportQuery request, + CancellationToken cancellationToken) + { + MerchantDetailsModel merchant = this.ApplicationCache.GetMerchantDetails(); + if (merchant == null) + { + return Result.Failure("Merchant details are not available."); + } + + return await this.ReportsService.GetRecentActivityReceiptReport(merchant.MerchantReportingId, + request.ReportDate.Date, + request.SearchText, + request.PageNumber, + request.PageSize, + cancellationToken); + } } diff --git a/TransactionProcessor.Mobile.BusinessLogic/Requests/ReportQueries.cs b/TransactionProcessor.Mobile.BusinessLogic/Requests/ReportQueries.cs index 21ea2493..10bfcf17 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Requests/ReportQueries.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Requests/ReportQueries.cs @@ -13,4 +13,9 @@ public record GetTransactionMixSummaryQuery(DateTime StartDate, TransactionMixBreakdown Breakdown, TransactionMixMeasure Measure, int TopN) : IRequest>; + + public record GetRecentActivityReceiptReportQuery(DateTime ReportDate, + string? SearchText, + int PageNumber = 1, + int PageSize = 5) : IRequest>; } diff --git a/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs b/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs index aa1dd8ee..e3603131 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/Services/ReportsService.cs @@ -20,6 +20,16 @@ Task> GetTransactionMixSummary(int merchantRe TransactionMixMeasure measure, int topN, CancellationToken cancellationToken); + Task> GetRecentActivityReceiptReport(int merchantReportingId, + DateTime reportDate, + string? searchText, + int pageNumber, + int pageSize, + CancellationToken cancellationToken); + + Task> ResendRecentActivityReceipt(string reference, + string recipientEmailAddress, + CancellationToken cancellationToken); } public class ReportsService : ClientProxyBase.ClientProxyBase, IReportsService { @@ -135,6 +145,158 @@ public async Task> GetTransactionMixSummary(i return Result.Success(model); } + + public async Task> GetRecentActivityReceiptReport(int merchantReportingId, + DateTime reportDate, + string? searchText, + int pageNumber, + int pageSize, + CancellationToken cancellationToken) + { + TokenResponseModel accessToken = this.ApplicationCache.GetAccessToken(); + + String requestUri = this.BuildRequestUrl("/api/reporting/recentactivityreceiptsearch"); + + RecentActivityReceiptSearchRequest requestBody = new() + { + ApplicationVersion = this.ApplicationInfoService.VersionString, + MerchantReportingId = merchantReportingId, + ReportDate = reportDate.Date, + SearchText = string.IsNullOrWhiteSpace(searchText) ? null : searchText.Trim(), + PageNumber = pageNumber > 0 ? pageNumber : 1, + PageSize = pageSize > 0 ? pageSize : 5 + }; + + Result? responseDataResult = await this.Post(requestUri, requestBody, accessToken.AccessToken, cancellationToken); + + if (responseDataResult.IsFailed) + { + Logger.LogInformation($"GetRecentActivityReceiptReport failed {responseDataResult.Status}"); + return ResultHelpers.CreateFailure(responseDataResult); + } + + RecentActivityReceiptReportModel model = new() + { + ReportDate = responseDataResult.Data.ReportDate, + SearchText = requestBody.SearchText, + PageNumber = responseDataResult.Data.PageNumber, + PageSize = responseDataResult.Data.PageSize, + TotalCount = responseDataResult.Data.TotalCount, + Items = responseDataResult.Data.Items.Select(item => new RecentActivityReceiptItemModel(item.Reference, + item.TransactionType, + item.Product, + item.Operator, + item.Status, + item.Amount, + item.TransactionDateTime, + item.ReceiptReference)).ToList() + }; + + return Result.Success(model); + } + + public async Task> ResendRecentActivityReceipt(string reference, + string recipientEmailAddress, + CancellationToken cancellationToken) + { + TokenResponseModel accessToken = this.ApplicationCache.GetAccessToken(); + + String requestUri = this.BuildRequestUrl($"/api/transactions/resendreceipt?applicationVersion={this.ApplicationInfoService.VersionString}"); + + ResendReceiptRequestMessage requestBody = new() + { + Reference = reference, + RecipientEmailAddress = recipientEmailAddress + }; + + Result? responseDataResult = await this.Post(requestUri, requestBody, accessToken.AccessToken, cancellationToken); + + if (responseDataResult.IsFailed) + { + Logger.LogInformation($"ResendRecentActivityReceipt failed {responseDataResult.Status}"); + return ResultHelpers.CreateFailure(responseDataResult); + } + + RecentActivityReceiptResendResultModel model = new() + { + Success = responseDataResult.Data.Success, + Message = responseDataResult.Data.Message, + Reference = responseDataResult.Data.Reference, + ReceiptReference = responseDataResult.Data.ReceiptReference, + TransactionReference = responseDataResult.Data.TransactionReference + }; + + return model.Success + ? Result.Success(model) + : Result.Failure(model.Message ?? "Unable to resend the receipt."); + } + } + + public class RecentActivityReceiptSearchRequest + { + public string ApplicationVersion { get; set; } + + public int MerchantReportingId { get; set; } + + public DateTime ReportDate { get; set; } + + public string? SearchText { get; set; } + + public int PageNumber { get; set; } = 1; + + public int PageSize { get; set; } = 5; + } + + public class RecentActivityReceiptSearchResponseDto + { + public DateTime ReportDate { get; set; } + + public int PageNumber { get; set; } + + public int PageSize { get; set; } + + public int TotalCount { get; set; } + + public List Items { get; set; } = []; + } + + public class RecentActivityReceiptSearchItemDto + { + public string Reference { get; set; } + + public string TransactionType { get; set; } + + public string Product { get; set; } + + public string Operator { get; set; } + + public string Status { get; set; } + + public decimal Amount { get; set; } + + public DateTime TransactionDateTime { get; set; } + + public string ReceiptReference { get; set; } + } + + public class RecentActivityReceiptResendResponseDto + { + public bool Success { get; set; } + + public string Message { get; set; } + + public string Reference { get; set; } + + public string ReceiptReference { get; set; } + + public string TransactionReference { get; set; } + } + + public class ResendReceiptRequestMessage + { + public string Reference { get; set; } + + public string RecipientEmailAddress { get; set; } } public class MerchantDailyPerformanceSummaryRequest diff --git a/TransactionProcessor.Mobile.BusinessLogic/UIServices/IDialogService.cs b/TransactionProcessor.Mobile.BusinessLogic/UIServices/IDialogService.cs index e884a31d..bffb0390 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/UIServices/IDialogService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/UIServices/IDialogService.cs @@ -26,6 +26,12 @@ Task ShowInformationToast(String message, TimeSpan? duration = null, CancellationToken cancellationToken = default); + Task ShowSuccessToast(String message, + Action? action = null, + String? actionButtonText = "OK", + TimeSpan? duration = null, + CancellationToken cancellationToken = default); + Task ShowWarningToast(String message, Action? action = null, String? actionButtonText = "OK", @@ -33,4 +39,4 @@ Task ShowWarningToast(String message, CancellationToken cancellationToken = default); #endregion -} \ No newline at end of file +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/UIServices/INavigationService.cs b/TransactionProcessor.Mobile.BusinessLogic/UIServices/INavigationService.cs index 90a55924..307b45d0 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/UIServices/INavigationService.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/UIServices/INavigationService.cs @@ -1,11 +1,9 @@ -using TransactionProcessor.Mobile.BusinessLogic.ViewModels.Transactions; +using TransactionProcessor.Mobile.BusinessLogic.ViewModels.Transactions; namespace TransactionProcessor.Mobile.BusinessLogic.UIServices; public interface INavigationService { - #region Methods - Task QuitApplication(); Task GoBack(); @@ -14,7 +12,7 @@ public interface INavigationService Task GoToMobileTopupFailedPage(); Task GoToMobileTopupPerformTopupPage(ProductDetails productDetails, - Decimal topupAmount); + Decimal topupAmount); Task GoToMobileTopupSelectOperatorPage(); @@ -43,7 +41,7 @@ Task GoToMobileTopupPerformTopupPage(ProductDetails productDetails, Task GoToVoucherSelectProductPage(ProductDetails productDetails); Task GoToVoucherIssueVoucherPage(ProductDetails productDetails, - Decimal voucherAmount); + Decimal voucherAmount); Task GoToBillPaymentGetAccountPage(ProductDetails productDetails); @@ -66,7 +64,9 @@ Task GoToBillPaymentPayBillPage(ProductDetails productDetails, Task GoToTransactionMixSummaryPage(); - Task GoToTransactions(); + Task GoToRecentActivityReportPage(); - #endregion + Task GoToRecentActivityReceiptDetailPage(TransactionProcessor.Mobile.BusinessLogic.Models.RecentActivityReceiptItemModel item); + + Task GoToTransactions(); } diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReceiptDetailPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReceiptDetailPageViewModel.cs new file mode 100644 index 00000000..ecab772e --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReceiptDetailPageViewModel.cs @@ -0,0 +1,151 @@ +using CommunityToolkit.Mvvm.Input; +using TransactionProcessor.Mobile.BusinessLogic.Common; +using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Services; +using TransactionProcessor.Mobile.BusinessLogic.UIServices; +using SimpleResults; + +namespace TransactionProcessor.Mobile.BusinessLogic.ViewModels.Reports; + +public sealed partial class RecentActivityReceiptDetailPageViewModel : ExtendedBaseViewModel +{ + private RecentActivityReceiptItemModel? selectedItem; + private string? emailAddress; + private bool isResendingReceipt; + private readonly IReportsService reportsService; + + public RecentActivityReceiptDetailPageViewModel(INavigationService navigationService, + IApplicationCache applicationCache, + IDialogService dialogService, + IDeviceService deviceService, + INavigationParameterService navigationParameterService, + IReportsService reportsService) : base(applicationCache, dialogService, navigationService, deviceService, navigationParameterService) + { + this.Title = "Receipt Detail"; + this.reportsService = reportsService; + } + + public RecentActivityReceiptItemModel? SelectedItem + { + get => this.selectedItem; + private set + { + if (SetProperty(ref this.selectedItem, value)) + { + this.OnPropertyChanged(nameof(this.Reference)); + this.OnPropertyChanged(nameof(this.TransactionType)); + this.OnPropertyChanged(nameof(this.Product)); + this.OnPropertyChanged(nameof(this.Operator)); + this.OnPropertyChanged(nameof(this.Status)); + this.OnPropertyChanged(nameof(this.Amount)); + this.OnPropertyChanged(nameof(this.TransactionDateTime)); + this.OnPropertyChanged(nameof(this.ReceiptReference)); + this.OnPropertyChanged(nameof(this.HasReceipt)); + this.ResendReceiptCommand.NotifyCanExecuteChanged(); + } + } + } + + public string? EmailAddress + { + get => this.emailAddress; + set + { + if (SetProperty(ref this.emailAddress, value)) + { + this.ResendReceiptCommand.NotifyCanExecuteChanged(); + } + } + } + + public bool IsResendingReceipt + { + get => this.isResendingReceipt; + private set + { + if (SetProperty(ref this.isResendingReceipt, value)) + { + this.ResendReceiptCommand.NotifyCanExecuteChanged(); + } + } + } + + public string Reference => this.SelectedItem?.Reference ?? string.Empty; + + public string TransactionType => this.SelectedItem?.TransactionType ?? string.Empty; + + public string Product => this.SelectedItem?.Product ?? string.Empty; + + public string Operator => this.SelectedItem?.Operator ?? string.Empty; + + public string Status => this.SelectedItem?.Status ?? string.Empty; + + public string Amount => this.SelectedItem is null ? string.Empty : this.SelectedItem.Amount.ToString("N2"); + + public string TransactionDateTime => this.SelectedItem is null ? string.Empty : this.SelectedItem.TransactionDateTime.ToString("dd MMM yyyy HH:mm"); + + public string ReceiptReference => this.SelectedItem?.ReceiptReference ?? string.Empty; + + public bool HasReceipt => this.SelectedItem is not null; + + [RelayCommand(CanExecute = nameof(CanResendReceipt))] + private async Task ResendReceipt() + { + if (this.CanResendReceipt() == false || this.IsValidEmailAddress(this.EmailAddress) == false) + { + await this.DialogService.ShowWarningToast("Enter a valid email address before resending the receipt."); + return; + } + + this.IsResendingReceipt = true; + try + { + string recipientEmailAddress = this.EmailAddress!.Trim(); + Result result = await this.reportsService.ResendRecentActivityReceipt(this.Reference, + recipientEmailAddress, + CancellationToken.None); + + if (result.IsFailed) + { + await this.DialogService.ShowWarningToast(result.Message ?? "Unable to resend the receipt."); + return; + } + + string message = string.IsNullOrWhiteSpace(result.Data.Message) + ? $"Receipt sent to {recipientEmailAddress}." + : $"{result.Data.Message} Sent to {recipientEmailAddress}."; + + this.EmailAddress = string.Empty; + await this.DialogService.ShowSuccessToast(message); + } + finally + { + this.IsResendingReceipt = false; + } + } + + private bool CanResendReceipt() + { + return this.HasReceipt + && this.IsResendingReceipt == false + && string.IsNullOrWhiteSpace(this.EmailAddress) == false; + } + + private bool IsValidEmailAddress(string? emailAddress) + { + return string.IsNullOrWhiteSpace(emailAddress) == false + && emailAddress.Contains('@', StringComparison.OrdinalIgnoreCase) + && emailAddress.Contains('.', StringComparison.OrdinalIgnoreCase); + } + + public override async Task Initialise(CancellationToken cancellationToken) + { + await base.Initialise(cancellationToken); + + IDictionary parameters = this.NavigationParameterService.GetParameters(); + if (parameters.TryGetValue(nameof(RecentActivityReceiptItemModel), out object? item) && item is RecentActivityReceiptItemModel selectedItem) + { + this.SelectedItem = selectedItem; + } + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReportPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReportPageViewModel.cs new file mode 100644 index 00000000..d32a3419 --- /dev/null +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/RecentActivityReportPageViewModel.cs @@ -0,0 +1,189 @@ +using CommunityToolkit.Mvvm.Input; +using MediatR; +using SimpleResults; +using TransactionProcessor.Mobile.BusinessLogic.Common; +using TransactionProcessor.Mobile.BusinessLogic.Models; +using TransactionProcessor.Mobile.BusinessLogic.Requests; +using TransactionProcessor.Mobile.BusinessLogic.Services; +using TransactionProcessor.Mobile.BusinessLogic.UIServices; + +namespace TransactionProcessor.Mobile.BusinessLogic.ViewModels.Reports; + +public sealed partial class RecentActivityReportPageViewModel : ExtendedBaseViewModel +{ + private const int DefaultPageSize = 5; + private readonly IMediator Mediator; + private DateTime selectedDate; + private string? searchText; + private bool isLoading; + private string? errorMessage; + private RecentActivityReceiptReportModel? report; + private int pageNumber = 1; + + public RecentActivityReportPageViewModel(IMediator mediator, + INavigationService navigationService, + IApplicationCache applicationCache, + IDialogService dialogService, + IDeviceService deviceService, + INavigationParameterService navigationParameterService) : base(applicationCache, dialogService, navigationService, deviceService, navigationParameterService) + { + this.Mediator = mediator; + this.Title = "Recent Activity and Receipt Report"; + this.SelectedDate = DateTime.Today; + } + + public DateTime SelectedDate + { + get => this.selectedDate; + set => SetProperty(ref this.selectedDate, value); + } + + public string? SearchText + { + get => this.searchText; + set => SetProperty(ref this.searchText, value); + } + + public bool IsLoading + { + get => this.isLoading; + set => SetProperty(ref this.isLoading, value); + } + + public string? ErrorMessage + { + get => this.errorMessage; + set => SetProperty(ref this.errorMessage, value); + } + + public RecentActivityReceiptReportModel? Report + { + get => this.report; + private set => SetProperty(ref this.report, value); + } + + public int PageNumber + { + get => this.pageNumber; + private set + { + if (SetProperty(ref this.pageNumber, value)) + { + this.OnPropertyChanged(nameof(this.PageDisplayText)); + this.OnPropertyChanged(nameof(this.CanGoToPreviousPage)); + this.OnPropertyChanged(nameof(this.CanGoToNextPage)); + this.OnPropertyChanged(nameof(this.HasPagination)); + this.PreviousPageCommand.NotifyCanExecuteChanged(); + this.NextPageCommand.NotifyCanExecuteChanged(); + } + } + } + + public int PageSize => DefaultPageSize; + + public IReadOnlyList Items => this.Report?.Items ?? []; + + public bool HasResults => this.Items.Count > 0; + + public bool HasError => string.IsNullOrWhiteSpace(this.ErrorMessage) == false; + + public bool HasEmptyState => this.IsLoading == false && this.HasError == false && this.HasResults == false; + + public int TotalCount => this.Report?.TotalCount ?? 0; + + public int TotalPages => this.Report?.TotalPages ?? 0; + + public bool CanGoToPreviousPage => this.PageNumber > 1; + + public bool CanGoToNextPage => this.TotalPages > 0 && this.PageNumber < this.TotalPages; + + public bool HasPagination => this.TotalPages > 1; + + public string PageDisplayText => this.TotalPages <= 0 + ? string.Empty + : $"Page {this.PageNumber} of {this.TotalPages}"; + + public override async Task Initialise(CancellationToken cancellationToken) + { + await base.Initialise(cancellationToken); + await this.LoadReportAsync(1, cancellationToken); + } + + [RelayCommand] + private async Task Search() + { + await this.LoadReportAsync(1, CancellationToken.None); + } + + [RelayCommand(CanExecute = nameof(CanGoToPreviousPage))] + private async Task PreviousPage() + { + await this.LoadReportAsync(this.PageNumber - 1, CancellationToken.None); + } + + [RelayCommand(CanExecute = nameof(CanGoToNextPage))] + private async Task NextPage() + { + await this.LoadReportAsync(this.PageNumber + 1, CancellationToken.None); + } + + [RelayCommand] + private async Task OpenItem(RecentActivityReceiptItemModel item) + { + await this.NavigationService.GoToRecentActivityReceiptDetailPage(item); + } + + private async Task LoadReportAsync(int pageNumber, CancellationToken cancellationToken) + { + this.IsLoading = true; + this.ErrorMessage = null; + + try + { + int requestedPageNumber = pageNumber > 0 ? pageNumber : 1; + Result result = await this.Mediator.Send(new ReportQueries.GetRecentActivityReceiptReportQuery( + this.SelectedDate.Date, + string.IsNullOrWhiteSpace(this.SearchText) ? null : this.SearchText.Trim(), + requestedPageNumber, + DefaultPageSize), + cancellationToken); + if (result.IsFailed) + { + this.Report = null; + this.ErrorMessage = "Unable to load recent activity."; + return; + } + + this.Report = result.Data; + this.PageNumber = result.Data.PageNumber > 0 ? result.Data.PageNumber : requestedPageNumber; + this.OnPropertyChanged(nameof(this.Items)); + this.OnPropertyChanged(nameof(this.HasResults)); + this.OnPropertyChanged(nameof(this.HasEmptyState)); + this.OnPropertyChanged(nameof(this.TotalCount)); + this.OnPropertyChanged(nameof(this.TotalPages)); + this.OnPropertyChanged(nameof(this.PageDisplayText)); + this.OnPropertyChanged(nameof(this.CanGoToPreviousPage)); + this.OnPropertyChanged(nameof(this.CanGoToNextPage)); + this.OnPropertyChanged(nameof(this.HasPagination)); + } + catch + { + this.Report = null; + this.ErrorMessage = "Unable to load recent activity."; + } + finally + { + this.IsLoading = false; + this.OnPropertyChanged(nameof(this.Items)); + this.OnPropertyChanged(nameof(this.HasResults)); + this.OnPropertyChanged(nameof(this.HasError)); + this.OnPropertyChanged(nameof(this.HasEmptyState)); + this.OnPropertyChanged(nameof(this.TotalCount)); + this.OnPropertyChanged(nameof(this.TotalPages)); + this.OnPropertyChanged(nameof(this.PageDisplayText)); + this.OnPropertyChanged(nameof(this.CanGoToPreviousPage)); + this.OnPropertyChanged(nameof(this.CanGoToNextPage)); + this.OnPropertyChanged(nameof(this.HasPagination)); + } + } +} diff --git a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/ReportsPageViewModel.cs b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/ReportsPageViewModel.cs index bf04c7d5..198319bd 100644 --- a/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/ReportsPageViewModel.cs +++ b/TransactionProcessor.Mobile.BusinessLogic/ViewModels/Reports/ReportsPageViewModel.cs @@ -50,7 +50,10 @@ public override async Task Initialise(CancellationToken cancellationToken) new ListViewItem { Title = "Transaction Mix" }, - }; + new ListViewItem { + Title = "Recent Activity and Receipt Report" + }, + }; await base.Initialise(cancellationToken); } @@ -64,6 +67,7 @@ private async Task OptionSelected(ItemSelected arg) { ReportsOptions.DailyPerformanceSummary => this.NavigationService.GoToDailyPerformanceSummaryPage(), ReportsOptions.TransactionMix => this.NavigationService.GoToTransactionMixSummaryPage(), + ReportsOptions.RecentActivityAndReceiptReport => this.NavigationService.GoToRecentActivityReportPage(), _ => Task.Factory.StartNew(() => Logger.LogWarning($"Unsupported option selected {selectedOption}")) }; @@ -79,6 +83,8 @@ public enum ReportsOptions DailyPerformanceSummary = 0, TransactionMix = 1, + + RecentActivityAndReceiptReport = 2, } #endregion diff --git a/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReceiptDetailPage.cs b/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReceiptDetailPage.cs new file mode 100644 index 00000000..db45a63e --- /dev/null +++ b/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReceiptDetailPage.cs @@ -0,0 +1,12 @@ +using TransactionProcessor.Mobile.UITests.Common; + +namespace TransactionProcessor.Mobile.UITests.Pages; + +public class RecentActivityReceiptDetailPage : BasePage2 +{ + protected override string Trait => "RecentActivityReceiptDetail"; + + public RecentActivityReceiptDetailPage(TestingContext testingContext) : base(testingContext) + { + } +} diff --git a/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReportPage.cs b/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReportPage.cs new file mode 100644 index 00000000..a52336c7 --- /dev/null +++ b/TransactionProcessor.Mobile.UITests/Pages/RecentActivityReportPage.cs @@ -0,0 +1,32 @@ +using OpenQA.Selenium; +using TransactionProcessor.Mobile.UITests.Common; + +namespace TransactionProcessor.Mobile.UITests.Pages; + +public class RecentActivityReportPage : BasePage2 +{ + protected override string Trait => "RecentActivityReport"; + + public RecentActivityReportPage(TestingContext testingContext) : base(testingContext) + { + } + + public async Task EnterSearchText(string searchText) + { + IWebElement element = await this.WaitForElementByAccessibilityId("RecentActivitySearchText"); + element.Clear(); + element.SendKeys(searchText); + } + + public async Task ClickSearchButton() + { + IWebElement element = await this.WaitForElementByAccessibilityId("RecentActivitySearchButton"); + element.Click(); + } + + public async Task ClickResult(string reference) + { + IWebElement element = await this.WaitForElementByAccessibilityId(reference); + element.Click(); + } +} diff --git a/TransactionProcessor.Mobile.UITests/Pages/ReportsPage.cs b/TransactionProcessor.Mobile.UITests/Pages/ReportsPage.cs index 007d5bbf..df0ebfdd 100644 --- a/TransactionProcessor.Mobile.UITests/Pages/ReportsPage.cs +++ b/TransactionProcessor.Mobile.UITests/Pages/ReportsPage.cs @@ -20,6 +20,7 @@ protected override String Trait private readonly String DailyPerformanceSummaryButton; private readonly String TransactionMixButton; + private readonly String RecentActivityAndReceiptReportButton; @@ -27,6 +28,7 @@ public ReportsPage(TestingContext testingContext) : base(testingContext) { this.DailyPerformanceSummaryButton = "DailyPerformanceSummaryButton"; this.TransactionMixButton = "TransactionMixButton"; + this.RecentActivityAndReceiptReportButton = "RecentActivityAndReceiptReportButton"; } public async Task ClickDailyPerformanceSummaryButton() @@ -41,6 +43,12 @@ public async Task ClickTransactionMixButton() element.Click(); } + public async Task ClickRecentActivityAndReceiptReportButton() + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.RecentActivityAndReceiptReportButton); + element.Click(); + } + } public class SupportPage : BasePage2{ diff --git a/TransactionProcessor.Mobile.UITests/Steps/ReportsSteps.cs b/TransactionProcessor.Mobile.UITests/Steps/ReportsSteps.cs index 29800334..a2861b51 100644 --- a/TransactionProcessor.Mobile.UITests/Steps/ReportsSteps.cs +++ b/TransactionProcessor.Mobile.UITests/Steps/ReportsSteps.cs @@ -9,11 +9,19 @@ public class ReportsSteps{ private readonly ReportsPage ReportsPage; private readonly DailyPerformanceSummaryPage DailyPerformanceSummaryPage; private readonly TransactionMixPage TransactionMixPage; + private readonly RecentActivityReportPage RecentActivityReportPage; + private readonly RecentActivityReceiptDetailPage RecentActivityReceiptDetailPage; - public ReportsSteps(ReportsPage reportsPage, DailyPerformanceSummaryPage dailyPerformanceSummaryPage, TransactionMixPage transactionMixPage){ + public ReportsSteps(ReportsPage reportsPage, + DailyPerformanceSummaryPage dailyPerformanceSummaryPage, + TransactionMixPage transactionMixPage, + RecentActivityReportPage recentActivityReportPage, + RecentActivityReceiptDetailPage recentActivityReceiptDetailPage){ this.ReportsPage = reportsPage; this.DailyPerformanceSummaryPage = dailyPerformanceSummaryPage; this.TransactionMixPage = transactionMixPage; + this.RecentActivityReportPage = recentActivityReportPage; + this.RecentActivityReceiptDetailPage = recentActivityReceiptDetailPage; } [Then(@"the Reports Page is displayed")] @@ -34,6 +42,12 @@ public async Task WhenITapOnTheTransactionMixButton() await this.ReportsPage.ClickTransactionMixButton(); } + [When(@"I tap on the Recent Activity and Receipt Report Button")] + public async Task WhenITapOnTheRecentActivityAndReceiptReportButton() + { + await this.ReportsPage.ClickRecentActivityAndReceiptReportButton(); + } + [Then(@"the Daily Performance Summary Report is displayed")] public async Task ThenTheDailyPerformanceSummaryReportIsDisplayed() { @@ -47,6 +61,30 @@ public async Task ThenTheTransactionMixReportIsDisplayed() await this.TransactionMixPage.AssertSummaryVisible(); } + [Then(@"the Recent Activity Report is displayed")] + public async Task ThenTheRecentActivityReportIsDisplayed() + { + await this.RecentActivityReportPage.AssertOnPage(); + } + + [When(@"I tap on Search on the Recent Activity Report")] + public async Task WhenITapOnSearchOnTheRecentActivityReport() + { + await this.RecentActivityReportPage.ClickSearchButton(); + } + + [When(@"I tap on the Recent Activity result for '(.*)'")] + public async Task WhenITapOnTheRecentActivityResultFor(string reference) + { + await this.RecentActivityReportPage.ClickResult(reference); + } + + [Then(@"the Recent Activity Receipt Detail Page is displayed")] + public async Task ThenTheRecentActivityReceiptDetailPageIsDisplayed() + { + await this.RecentActivityReceiptDetailPage.AssertOnPage(); + } + [Then(@"the Transaction Mix Chart is displayed")] public async Task ThenTheTransactionMixChartIsDisplayed() { diff --git a/TransactionProcessor.Mobile/App.xaml.cs b/TransactionProcessor.Mobile/App.xaml.cs index ebd00fa8..c697b4b3 100644 --- a/TransactionProcessor.Mobile/App.xaml.cs +++ b/TransactionProcessor.Mobile/App.xaml.cs @@ -179,6 +179,8 @@ public App(IApplicationThemeService applicationThemeService) RegisterRouteOnce(nameof(DailyPerformanceSummaryPage), typeof(DailyPerformanceSummaryPage)); RegisterRouteOnce(nameof(TransactionMixPage), typeof(TransactionMixPage)); + RegisterRouteOnce(nameof(RecentActivityReportPage), typeof(RecentActivityReportPage)); + RegisterRouteOnce(nameof(RecentActivityReceiptDetailPage), typeof(RecentActivityReceiptDetailPage)); RegisterRouteOnce(nameof(MyAccountAddressesPage), typeof(MyAccountAddressesPage)); RegisterRouteOnce(nameof(MyAccountContactPage), typeof(MyAccountContactPage)); diff --git a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs index 1a54a568..44ec364c 100644 --- a/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs +++ b/TransactionProcessor.Mobile/Extensions/MauiAppBuilderExtensions.cs @@ -229,6 +229,8 @@ public static MauiAppBuilder ConfigureViewModels(this MauiAppBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder; } @@ -271,6 +273,8 @@ public static MauiAppBuilder ConfigurePages(this MauiAppBuilder builder) { builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); return builder; } diff --git a/TransactionProcessor.Mobile/Pages/Reports/RecentActivityReceiptDetailPage.xaml b/TransactionProcessor.Mobile/Pages/Reports/RecentActivityReceiptDetailPage.xaml new file mode 100644 index 00000000..19260057 --- /dev/null +++ b/TransactionProcessor.Mobile/Pages/Reports/RecentActivityReceiptDetailPage.xaml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + +