diff --git a/TransactionProcessorACL.BusinessLogic.Tests/MediatorTests.cs b/TransactionProcessorACL.BusinessLogic.Tests/MediatorTests.cs index d0a9d7b..9fa99de 100644 --- a/TransactionProcessorACL.BusinessLogic.Tests/MediatorTests.cs +++ b/TransactionProcessorACL.BusinessLogic.Tests/MediatorTests.cs @@ -39,6 +39,7 @@ public MediatorTests() this.Requests.Add(TestData.GetMerchantScheduleQuery); this.Requests.Add(TestData.GetMerchantDailyPerformanceSummaryQuery); this.Requests.Add(TestData.GetMerchantTransactionMixSummaryQuery); + this.Requests.Add(TestData.GetRecentActivityReceiptSearchQuery); } [Fact] @@ -177,5 +178,12 @@ public async Task> GetMerchantTran { return Result.Success(new MerchantTransactionMixSummaryResponse()); } + + public async Task> GetRecentActivityReceiptSearch(Guid estateId, + RecentActivityReceiptSearchRequest request, + CancellationToken cancellationToken) + { + return Result.Success(new RecentActivityReceiptSearchResponse()); + } } } diff --git a/TransactionProcessorACL.BusinessLogic.Tests/RequestHandlerTests.cs b/TransactionProcessorACL.BusinessLogic.Tests/RequestHandlerTests.cs index 31a7862..8097c11 100644 --- a/TransactionProcessorACL.BusinessLogic.Tests/RequestHandlerTests.cs +++ b/TransactionProcessorACL.BusinessLogic.Tests/RequestHandlerTests.cs @@ -256,6 +256,35 @@ public async Task ReportingRequestHandler_GetMerchantDailyPerformanceSummaryQuer result.Data.ShouldNotBeNull(); } + [Fact] + public async Task ReportingRequestHandler_GetRecentActivityReceiptSearchQuery_Handle_RequestIsHandled() + { + Mock applicationService = new Mock(); + applicationService + .Setup(a => a.GetRecentActivityReceiptSearch(It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(new RecentActivityReceiptSearchResponse()); + + ReportingRequestHandler requestHandler = new ReportingRequestHandler(applicationService.Object); + + ReportingQueries.GetRecentActivityReceiptSearchQuery query = new( + TestData.EstateId, + new RecentActivityReceiptSearchRequest + { + MerchantReportingId = 12345, + ReportDate = new DateTime(2026, 7, 8), + SearchText = "abc", + PageNumber = 2, + PageSize = 5 + }); + + Result result = await requestHandler.Handle(query, CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBeNull(); + } + #endregion } } diff --git a/TransactionProcessorACL.BusinessLogic.Tests/TransactionProcessorACLApplicationServiceTests.cs b/TransactionProcessorACL.BusinessLogic.Tests/TransactionProcessorACLApplicationServiceTests.cs index 21408de..b9b9138 100644 --- a/TransactionProcessorACL.BusinessLogic.Tests/TransactionProcessorACLApplicationServiceTests.cs +++ b/TransactionProcessorACL.BusinessLogic.Tests/TransactionProcessorACLApplicationServiceTests.cs @@ -699,5 +699,86 @@ public async Task TransactionProcessorACLApplicationService_GetMerchantTransacti capturedRequest.MerchantReportingId.ShouldBe(12345); //capturedRequest.Breakdown.ShouldBe(RequestTransactionMixBreakdown.Product); } + + [Fact] + public async Task TransactionProcessorACLApplicationService_GetRecentActivityReceiptSearch_ReturnedFromEstateReportingClient() + { + TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects.RecentActivityReceiptSearchRequest capturedRequest = null; + estateReportingApiClient + .Setup(v => v.GetRecentActivityReceiptSearch(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, request, _) => capturedRequest = request) + .ReturnsAsync(Result.Success(new TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects.RecentActivityReceiptSearchResponse + { + ReportDate = new DateTime(2026, 7, 8), + PageNumber = 2, + PageSize = 5, + TotalCount = 1, + Items = + [ + new TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects.RecentActivityReceiptSearchItem + { + Reference = "REF-1", + TransactionType = "SALE", + Product = "Product 1", + Operator = "Operator 1", + Status = "Completed", + Amount = 42.75M, + TransactionDateTime = new DateTime(2026, 7, 8, 10, 30, 0), + ReceiptReference = "RCPT-1" + } + ] + })); + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(Result.Success(TestData.TokenResponse)); + + Result result = await applicationService.GetRecentActivityReceiptSearch( + TestData.EstateId, + new RecentActivityReceiptSearchRequest + { + MerchantReportingId = 12345, + ReportDate = new DateTime(2026, 7, 8), + SearchText = "abc", + PageNumber = 2, + PageSize = 5 + }, + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + result.Data.ShouldNotBeNull(); + result.Data.Items.Count.ShouldBe(1); + result.Data.Items[0].ReceiptReference.ShouldBe("RCPT-1"); + capturedRequest.ShouldNotBeNull(); + capturedRequest.MerchantReportingId.ShouldBe(12345); + capturedRequest.ReportDate.ShouldBe(new DateTime(2026, 7, 8)); + capturedRequest.SearchText.ShouldBe("abc"); + capturedRequest.PageNumber.ShouldBe(2); + capturedRequest.PageSize.ShouldBe(5); + } + + [Fact] + public async Task TransactionProcessorACLApplicationService_GetRecentActivityReceiptSearch_BlankSearchTextIsNotForwarded() + { + TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects.RecentActivityReceiptSearchRequest capturedRequest = null; + estateReportingApiClient + .Setup(v => v.GetRecentActivityReceiptSearch(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, request, _) => capturedRequest = request) + .ReturnsAsync(Result.Success(new TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects.RecentActivityReceiptSearchResponse())); + securityServiceClient.Setup(s => s.GetToken(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(Result.Success(TestData.TokenResponse)); + + Result result = await applicationService.GetRecentActivityReceiptSearch( + TestData.EstateId, + new RecentActivityReceiptSearchRequest + { + MerchantReportingId = 12345, + ReportDate = new DateTime(2026, 7, 8), + SearchText = " ", + PageNumber = 1, + PageSize = 5 + }, + CancellationToken.None); + + result.IsSuccess.ShouldBeTrue(); + capturedRequest.ShouldNotBeNull(); + capturedRequest.SearchText.ShouldBeNull(); + } } } diff --git a/TransactionProcessorACL.BusinessLogic/BackendAPI/DataTransferObjects/RecentActivityReceiptSearch.cs b/TransactionProcessorACL.BusinessLogic/BackendAPI/DataTransferObjects/RecentActivityReceiptSearch.cs new file mode 100644 index 0000000..d92da7f --- /dev/null +++ b/TransactionProcessorACL.BusinessLogic/BackendAPI/DataTransferObjects/RecentActivityReceiptSearch.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace TransactionProcessorACL.BusinessLogic.BackendAPI.DataTransferObjects; + +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 RecentActivityReceiptSearchResponse +{ + 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 RecentActivityReceiptSearchItem +{ + 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; } +} diff --git a/TransactionProcessorACL.BusinessLogic/BackendAPI/EstateReportingApiClient.cs b/TransactionProcessorACL.BusinessLogic/BackendAPI/EstateReportingApiClient.cs index 9d0b286..0db5112 100644 --- a/TransactionProcessorACL.BusinessLogic/BackendAPI/EstateReportingApiClient.cs +++ b/TransactionProcessorACL.BusinessLogic/BackendAPI/EstateReportingApiClient.cs @@ -80,6 +80,35 @@ public async Task> GetMerchantTransactionM } } + public async Task> GetRecentActivityReceiptSearch(String accessToken, + Guid estateId, + RecentActivityReceiptSearchRequest request, + CancellationToken cancellationToken) + { + string requestUri = this.BuildRequestUrl("/api/transactions/recentactivityreceiptreport"); + + try + { + List<(string headerName, string headerValue)> additionalHeaders = + [ + (EstateIdHeaderName, estateId.ToString()) + ]; + + Result result = + await this.Post(requestUri, request, accessToken, additionalHeaders, cancellationToken); + + if (result.IsFailed) + return ResultHelpers.CreateFailure(result); + + return result; + } + catch (Exception ex) + { + Exception exception = new($"Error getting recent activity receipt search for estate {estateId}.", ex); + return Result.Failure(exception.Message); + } + } + private string BuildRequestUrl(string relativePath) { string baseAddress = this.BaseAddressResolver("EstateReportingApi"); diff --git a/TransactionProcessorACL.BusinessLogic/BackendAPI/IEstateReportingApiClient.cs b/TransactionProcessorACL.BusinessLogic/BackendAPI/IEstateReportingApiClient.cs index 8cdca7f..4594b21 100644 --- a/TransactionProcessorACL.BusinessLogic/BackendAPI/IEstateReportingApiClient.cs +++ b/TransactionProcessorACL.BusinessLogic/BackendAPI/IEstateReportingApiClient.cs @@ -18,4 +18,9 @@ Task> GetMerchantTransactionMixSummary(Str Guid estateId, TransactionMixSummaryRequest request, CancellationToken cancellationToken); + + Task> GetRecentActivityReceiptSearch(String accessToken, + Guid estateId, + RecentActivityReceiptSearchRequest request, + CancellationToken cancellationToken); } diff --git a/TransactionProcessorACL.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs b/TransactionProcessorACL.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs index 33b88a9..8ed657b 100644 --- a/TransactionProcessorACL.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs +++ b/TransactionProcessorACL.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs @@ -11,7 +11,8 @@ namespace TransactionProcessorACL.BusinessLogic.RequestHandlers; public class ReportingRequestHandler : IRequestHandler>, - IRequestHandler> + IRequestHandler>, + IRequestHandler> { private readonly ITransactionProcessorACLApplicationService ApplicationService; @@ -31,4 +32,10 @@ public async Task> Handle(Reportin { return await this.ApplicationService.GetMerchantTransactionMixSummary(request.EstateId, request.Request, cancellationToken); } + + public async Task> Handle(ReportingQueries.GetRecentActivityReceiptSearchQuery request, + CancellationToken cancellationToken) + { + return await this.ApplicationService.GetRecentActivityReceiptSearch(request.EstateId, request.Request, cancellationToken); + } } diff --git a/TransactionProcessorACL.BusinessLogic/Requests/ReportingQueries.cs b/TransactionProcessorACL.BusinessLogic/Requests/ReportingQueries.cs index 9ccbdb2..5c549b1 100644 --- a/TransactionProcessorACL.BusinessLogic/Requests/ReportingQueries.cs +++ b/TransactionProcessorACL.BusinessLogic/Requests/ReportingQueries.cs @@ -17,4 +17,8 @@ public record GetMerchantDailyPerformanceSummaryQuery(Guid EstateId, public record GetMerchantTransactionMixSummaryQuery(Guid EstateId, MerchantTransactionMixSummaryRequest Request) : IRequest>; + + public record GetRecentActivityReceiptSearchQuery(Guid EstateId, + RecentActivityReceiptSearchRequest Request) + : IRequest>; } diff --git a/TransactionProcessorACL.BusinessLogic/Services/ITransactionProcessorACLApplicationService.cs b/TransactionProcessorACL.BusinessLogic/Services/ITransactionProcessorACLApplicationService.cs index a7f7a1d..881dbf2 100644 --- a/TransactionProcessorACL.BusinessLogic/Services/ITransactionProcessorACLApplicationService.cs +++ b/TransactionProcessorACL.BusinessLogic/Services/ITransactionProcessorACLApplicationService.cs @@ -69,5 +69,9 @@ Task> GetMerchantDailyPerformanc Task> GetMerchantTransactionMixSummary(Guid estateId, MerchantTransactionMixSummaryRequest request, CancellationToken cancellationToken); + + Task> GetRecentActivityReceiptSearch(Guid estateId, + RecentActivityReceiptSearchRequest request, + CancellationToken cancellationToken); } } diff --git a/TransactionProcessorACL.BusinessLogic/Services/TransactionProcessorACLApplicationService.cs b/TransactionProcessorACL.BusinessLogic/Services/TransactionProcessorACLApplicationService.cs index 2274bd0..5ba486b 100644 --- a/TransactionProcessorACL.BusinessLogic/Services/TransactionProcessorACLApplicationService.cs +++ b/TransactionProcessorACL.BusinessLogic/Services/TransactionProcessorACLApplicationService.cs @@ -446,6 +446,52 @@ public async Task> GetMerchantTran return response; } + public async Task> GetRecentActivityReceiptSearch(Guid estateId, + RecentActivityReceiptSearchRequest request, + CancellationToken cancellationToken) + { + Result accessTokenResult = await this.GetAccessToken(cancellationToken); + if (accessTokenResult.IsFailed) { + return ResultHelpers.CreateFailure(accessTokenResult); + } + + var apiRequest = new BackendAPI.DataTransferObjects.RecentActivityReceiptSearchRequest + { + MerchantReportingId = request.MerchantReportingId, + ReportDate = request.ReportDate.Date, + SearchText = string.IsNullOrWhiteSpace(request.SearchText) ? null : request.SearchText.Trim(), + PageNumber = request.PageNumber, + PageSize = request.PageSize + }; + + TokenResponse accessToken = accessTokenResult.Data; + var apiResponse = await this.EstateReportingApiClient.GetRecentActivityReceiptSearch(accessToken.AccessToken, estateId, apiRequest, cancellationToken); + + if (apiResponse.IsFailed) + return ResultHelpers.CreateFailure(apiResponse); + + RecentActivityReceiptSearchResponse response = new() + { + ReportDate = apiResponse.Data.ReportDate, + PageNumber = apiResponse.Data.PageNumber, + PageSize = apiResponse.Data.PageSize, + TotalCount = apiResponse.Data.TotalCount, + Items = apiResponse.Data.Items.Select(i => new RecentActivityReceiptSearchItem + { + Amount = i.Amount, + Operator = i.Operator, + Product = i.Product, + Reference = i.Reference, + ReceiptReference = i.ReceiptReference, + Status = i.Status, + TransactionDateTime = i.TransactionDateTime, + TransactionType = i.TransactionType + }).ToList() + }; + + return response; + } + private static ProcessReconciliationResponse CreateProcessReconciliationResponse(ReconciliationResponse reconciliationResponse) { return new ProcessReconciliationResponse diff --git a/TransactionProcessorACL.DataTransferObjects/Requests/RecentActivityReceiptSearchRequest.cs b/TransactionProcessorACL.DataTransferObjects/Requests/RecentActivityReceiptSearchRequest.cs new file mode 100644 index 0000000..56799e2 --- /dev/null +++ b/TransactionProcessorACL.DataTransferObjects/Requests/RecentActivityReceiptSearchRequest.cs @@ -0,0 +1,20 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace TransactionProcessorACL.DataTransferObjects.Requests; + +[ExcludeFromCodeCoverage] +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; +} diff --git a/TransactionProcessorACL.IntegrationTests/Common/GenericSteps.cs b/TransactionProcessorACL.IntegrationTests/Common/GenericSteps.cs index 3a264b1..d3f9147 100644 --- a/TransactionProcessorACL.IntegrationTests/Common/GenericSteps.cs +++ b/TransactionProcessorACL.IntegrationTests/Common/GenericSteps.cs @@ -41,11 +41,6 @@ public async Task StartSystem() DockerServices.SqlServer | DockerServices.TestHost | DockerServices.TransactionProcessor | DockerServices.TransactionProcessorAcl | DockerServices.EstateReporting; - if (this.ScenarioContext.ScenarioInfo.Tags.Any(tag => tag.Equals("reporting", StringComparison.OrdinalIgnoreCase))) - { - dockerServices |= DockerServices.EstateReporting; - } - this.TestingContext.DockerHelper = new DockerHelper(); this.TestingContext.DockerHelper.Logger = logger; this.TestingContext.Logger = logger; diff --git a/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature b/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature index edf8c64..542e6bf 100644 --- a/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature +++ b/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature @@ -127,3 +127,25 @@ Scenario: Merchant transaction mix summary When I get the merchant transaction mix summary for Merchant "Test Merchant 1" for Estate "Test Estate 1" Then the merchant transaction mix summary response should contain at least one item and the sale amount 25.00 + +@PRTest +Scenario: Recent activity receipt search supports paging + Given I am logged in as "merchantuser@testmerchant1.co.uk" with password "123456" for Merchant "Test Merchant 1" for Estate "Test Estate 1" with client "merchantClient" + When I perform the following transactions + | DateTime | TransactionNumber | TransactionType | MerchantName | DeviceIdentifier | EstateName | OperatorName | TransactionAmount | CustomerAccountNumber | CustomerEmailAddress | ContractDescription | ProductName | RecipientEmail | RecipientMobile | + | Today | 1001 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 10.00 | 123456789 | test1@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1002 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 11.00 | 123456789 | test2@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1003 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 12.00 | 123456789 | test3@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1004 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 13.00 | 123456789 | test4@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1005 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 14.00 | 123456789 | test5@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1006 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 15.00 | 123456789 | test6@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1007 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 16.00 | 123456789 | test7@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1008 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 17.00 | 123456789 | test8@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1009 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 18.00 | 123456789 | test9@customer.co.uk | Safaricom Contract | Variable Topup | | | + | Today | 1010 | Sale | Test Merchant 1 | 123456780 | Test Estate 1 | Safaricom | 19.00 | 123456789 | test10@customer.co.uk | Safaricom Contract | Variable Topup | | | + + When I get the recent activity receipt search for Merchant "Test Merchant 1" for Estate "Test Estate 1" page 1 size 5 + Then the recent activity receipt search response for page 1 should contain 5 items and total count 10 + + When I get the recent activity receipt search for Merchant "Test Merchant 1" for Estate "Test Estate 1" page 2 size 5 + Then the recent activity receipt search response for page 2 should contain 5 items and total count 10 diff --git a/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature.cs b/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature.cs index 6da3c19..c529e36 100644 --- a/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature.cs +++ b/TransactionProcessorACL.IntegrationTests/Reporting/Reporting.feature.cs @@ -543,7 +543,7 @@ await testRunner.GivenAsync("I have a token to access the estate management and private static global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages InitializeCucumberMessages() { - return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Reporting/Reporting.feature.ndjson", 4); + return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("Reporting/Reporting.feature.ndjson", 5); } [global::NUnit.Framework.TestAttribute()] @@ -691,6 +691,224 @@ await testRunner.ThenAsync("the merchant daily performance summary response shou #line 129 await testRunner.ThenAsync("the merchant transaction mix summary response should contain at least one item an" + "d the sale amount 25.00", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); +#line hidden + } + await this.ScenarioCleanupAsync(); + } + + [global::NUnit.Framework.TestAttribute()] + [global::NUnit.Framework.DescriptionAttribute("Recent activity receipt search supports paging")] + [global::NUnit.Framework.CategoryAttribute("PRTest")] + public async global::System.Threading.Tasks.Task RecentActivityReceiptSearchSupportsPaging() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary(); + string pickleIndex = "2"; + global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Recent activity receipt search supports paging", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex); + string[] tagsOfRule = ((string[])(null)); + global::Reqnroll.RuleInfo ruleInfo = null; +#line 132 +this.ScenarioInitialize(scenarioInfo, ruleInfo); +#line hidden + if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags))) + { + await testRunner.SkipScenarioAsync(); + } + else + { + await this.ScenarioStartAsync(); +#line 4 +await this.FeatureBackgroundAsync(); +#line hidden +#line 133 + await testRunner.GivenAsync("I am logged in as \"merchantuser@testmerchant1.co.uk\" with password \"123456\" for M" + + "erchant \"Test Merchant 1\" for Estate \"Test Estate 1\" with client \"merchantClient" + + "\"", ((string)(null)), ((global::Reqnroll.Table)(null)), "Given "); +#line hidden + global::Reqnroll.Table table20 = new global::Reqnroll.Table(new string[] { + "DateTime", + "TransactionNumber", + "TransactionType", + "MerchantName", + "DeviceIdentifier", + "EstateName", + "OperatorName", + "TransactionAmount", + "CustomerAccountNumber", + "CustomerEmailAddress", + "ContractDescription", + "ProductName", + "RecipientEmail", + "RecipientMobile"}); + table20.AddRow(new string[] { + "Today", + "1001", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "10.00", + "123456789", + "test1@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1002", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "11.00", + "123456789", + "test2@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1003", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "12.00", + "123456789", + "test3@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1004", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "13.00", + "123456789", + "test4@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1005", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "14.00", + "123456789", + "test5@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1006", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "15.00", + "123456789", + "test6@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1007", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "16.00", + "123456789", + "test7@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1008", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "17.00", + "123456789", + "test8@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1009", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "18.00", + "123456789", + "test9@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); + table20.AddRow(new string[] { + "Today", + "1010", + "Sale", + "Test Merchant 1", + "123456780", + "Test Estate 1", + "Safaricom", + "19.00", + "123456789", + "test10@customer.co.uk", + "Safaricom Contract", + "Variable Topup", + "", + ""}); +#line 134 + await testRunner.WhenAsync("I perform the following transactions", ((string)(null)), table20, "When "); +#line hidden +#line 147 + await testRunner.WhenAsync("I get the recent activity receipt search for Merchant \"Test Merchant 1\" for Estat" + + "e \"Test Estate 1\" page 1 size 5", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); +#line hidden +#line 148 + await testRunner.ThenAsync("the recent activity receipt search response for page 1 should contain 5 items and" + + " total count 10", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); +#line hidden +#line 150 + await testRunner.WhenAsync("I get the recent activity receipt search for Merchant \"Test Merchant 1\" for Estat" + + "e \"Test Estate 1\" page 2 size 5", ((string)(null)), ((global::Reqnroll.Table)(null)), "When "); +#line hidden +#line 151 + await testRunner.ThenAsync("the recent activity receipt search response for page 2 should contain 5 items and" + + " total count 10", ((string)(null)), ((global::Reqnroll.Table)(null)), "Then "); #line hidden } await this.ScenarioCleanupAsync(); diff --git a/TransactionProcessorACL.IntegrationTests/Shared/ACLSteps.cs b/TransactionProcessorACL.IntegrationTests/Shared/ACLSteps.cs index 1720dfb..d51a0b5 100644 --- a/TransactionProcessorACL.IntegrationTests/Shared/ACLSteps.cs +++ b/TransactionProcessorACL.IntegrationTests/Shared/ACLSteps.cs @@ -33,10 +33,14 @@ public class ACLSteps{ private Guid LastMerchantDailyPerformanceSummaryMerchantId; private EstateDetails1 LastMerchantTransactionMixSummaryEstateDetails; private Guid LastMerchantTransactionMixSummaryMerchantId; + private EstateDetails1 LastRecentActivityReceiptSearchEstateDetails; + private Guid LastRecentActivityReceiptSearchMerchantId; + private readonly Dictionary RecentActivityReceiptSearchResponses; public ACLSteps(HttpClient httpClient, ITransactionProcessorClient transactionProcessorClient){ this.HttpClient = httpClient; this.TransactionProcessorClient = transactionProcessorClient; + this.RecentActivityReceiptSearchResponses = new Dictionary(); } internal class ResponseData @@ -334,6 +338,50 @@ public async Task WhenIGetTheMerchantTransactionMixSummaryForMerchantForEstate(S this.LastMerchantTransactionMixSummaryMerchantId = merchantId; } + public async Task WhenIGetTheRecentActivityReceiptSearchForMerchantForEstate(String estateName, + String merchantName, + Int32 pageNumber, + Int32 pageSize, + List estateDetailsList, + CancellationToken cancellationToken) + { + EstateDetails1 es1 = estateDetailsList.SingleOrDefault(e => e.EstateDetails.EstateName == estateName); + es1.ShouldNotBeNull(); + Guid merchantId = es1.EstateDetails.GetMerchantId(merchantName); + Int32 merchantReportingId = es1.GetMerchantReportingId(merchantId); + + String uri = "api/reporting/recentactivityreceiptsearch"; + String userAccessToken = es1.GetMerchantUserToken(merchantId); + + this.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", userAccessToken); + + RecentActivityReceiptSearchRequest request = new() + { + ApplicationVersion = "1.0.5", + MerchantReportingId = merchantReportingId, + ReportDate = DateTime.Today, + PageNumber = pageNumber, + PageSize = pageSize + }; + + StringContent content = new StringContent(StringSerialiser.Serialise(request), Encoding.UTF8, "application/json"); + + HttpResponseMessage response = await this.HttpClient.PostAsync(uri, content, cancellationToken).ConfigureAwait(false); + + response.IsSuccessStatusCode.ShouldBeTrue(); + + String responseContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + responseContent.ShouldNotBeNullOrEmpty("No response message received"); + + RecentActivityReceiptSearchResponse searchResponse = + StringSerialiser.Deserialise(responseContent, + new SerialiserOptions(SerialiserPropertyFormat.SnakeCase)); + + this.RecentActivityReceiptSearchResponses[pageNumber] = searchResponse; + this.LastRecentActivityReceiptSearchEstateDetails = es1; + this.LastRecentActivityReceiptSearchMerchantId = merchantId; + } + public void ThenTheMerchantTransactionMixSummaryResponseShouldContainAtLeastOneItemAndTheSaleAmount(Decimal saleAmount) { this.LastMerchantTransactionMixSummaryEstateDetails.ShouldNotBeNull(); @@ -348,4 +396,19 @@ public void ThenTheMerchantTransactionMixSummaryResponseShouldContainAtLeastOneI merchantTransactionMixSummaryResponse.DrillDownTransactions.Any(t => t.Amount == saleAmount) .ShouldBeTrue($"Expected a drill down transaction with amount {saleAmount}"); } + + public void ThenTheRecentActivityReceiptSearchResponseShouldContainPageWithCountAndDescendingDates(Int32 pageNumber, + Int32 expectedCount, + Int32 expectedTotalCount) + { + this.LastRecentActivityReceiptSearchEstateDetails.ShouldNotBeNull(); + + RecentActivityReceiptSearchResponse response = this.RecentActivityReceiptSearchResponses.SingleOrDefault(x => x.Key == pageNumber).Value; + response.ShouldNotBeNull(); + response.Items.Count.ShouldBe(expectedCount); + response.TotalCount.ShouldBe(expectedTotalCount); + response.PageNumber.ShouldBe(pageNumber); + response.PageSize.ShouldBe(expectedCount); + response.Items.Select(i => i.TransactionDateTime).Zip(response.Items.Select(i => i.TransactionDateTime).Skip(1), (first, second) => first >= second).All(x => x).ShouldBeTrue(); + } } diff --git a/TransactionProcessorACL.IntegrationTests/Shared/ReportingDtos.cs b/TransactionProcessorACL.IntegrationTests/Shared/ReportingDtos.cs index 2e808e8..3e95f9e 100644 --- a/TransactionProcessorACL.IntegrationTests/Shared/ReportingDtos.cs +++ b/TransactionProcessorACL.IntegrationTests/Shared/ReportingDtos.cs @@ -50,3 +50,35 @@ public class TransactionMixDrillDownTransaction public DateTime TransactionDateTime { get; set; } } + +public class RecentActivityReceiptSearchResponse +{ + 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 RecentActivityReceiptSearchItem +{ + 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; } +} diff --git a/TransactionProcessorACL.IntegrationTests/Shared/SharedSteps.cs b/TransactionProcessorACL.IntegrationTests/Shared/SharedSteps.cs index d05d3f3..b282772 100644 --- a/TransactionProcessorACL.IntegrationTests/Shared/SharedSteps.cs +++ b/TransactionProcessorACL.IntegrationTests/Shared/SharedSteps.cs @@ -508,6 +508,30 @@ public void ThenTheMerchantTransactionMixSummaryResponseShouldContainAtLeastOneI this.AclSteps.ThenTheMerchantTransactionMixSummaryResponseShouldContainAtLeastOneItemAndTheSaleAmount(saleAmount); } + [When("I get the recent activity receipt search for Merchant {string} for Estate {string} page {int} size {int}")] + public async Task WhenIGetTheRecentActivityReceiptSearchForMerchantForEstatePageSize(string merchantName, + string estateName, + int pageNumber, + int pageSize) + { + await this.AclSteps.WhenIGetTheRecentActivityReceiptSearchForMerchantForEstate(estateName, + merchantName, + pageNumber, + pageSize, + this.TestingContext.Estates, + CancellationToken.None); + } + + [Then("the recent activity receipt search response for page {int} should contain {int} items and total count {int}")] + public void ThenTheRecentActivityReceiptSearchResponseForPageShouldContainItemsAndTotalCount(int pageNumber, + int expectedCount, + int expectedTotalCount) + { + this.AclSteps.ThenTheRecentActivityReceiptSearchResponseShouldContainPageWithCountAndDescendingDates(pageNumber, + expectedCount, + expectedTotalCount); + } + #endregion diff --git a/TransactionProcessorACL.Models/RecentActivityReceiptSearchResponse.cs b/TransactionProcessorACL.Models/RecentActivityReceiptSearchResponse.cs new file mode 100644 index 0000000..5f781f9 --- /dev/null +++ b/TransactionProcessorACL.Models/RecentActivityReceiptSearchResponse.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; + +namespace TransactionProcessorACL.Models; + +public class RecentActivityReceiptSearchResponse +{ + 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 RecentActivityReceiptSearchItem +{ + 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; } +} diff --git a/TransactionProcessorACL.Testing/TestData.cs b/TransactionProcessorACL.Testing/TestData.cs index 6098fea..16f9679 100644 --- a/TransactionProcessorACL.Testing/TestData.cs +++ b/TransactionProcessorACL.Testing/TestData.cs @@ -237,6 +237,15 @@ public class TestData Measure = TransactionProcessorACL.DataTransferObjects.Requests.TransactionMixMeasure.Count, TopN = 5 }); + public static ReportingQueries.GetRecentActivityReceiptSearchQuery GetRecentActivityReceiptSearchQuery => new(EstateId, + new RecentActivityReceiptSearchRequest + { + MerchantReportingId = 12345, + ReportDate = new DateTime(2026, 7, 8), + SearchText = "abc", + PageNumber = 2, + PageSize = 5 + }); public static VoucherQueries.GetVoucherQuery GetVoucherQuery => new(TestData.EstateId, TestData.ContractId, TestData.VoucherCode); diff --git a/TransactionProcessorACL.Tests/Handlers/ReportingHandlersTests.cs b/TransactionProcessorACL.Tests/Handlers/ReportingHandlersTests.cs index 68e4176..ff6b46e 100644 --- a/TransactionProcessorACL.Tests/Handlers/ReportingHandlersTests.cs +++ b/TransactionProcessorACL.Tests/Handlers/ReportingHandlersTests.cs @@ -83,4 +83,42 @@ public async Task GetMerchantTransactionMixSummary_PassesEstateClaimAndRequestTo capturedQuery.Request.Measure.ShouldBe(TransactionMixMeasure.Count); mediator.Verify(m => m.Send(It.IsAny(), It.IsAny()), Times.Once); } + + [Fact] + public async Task GetRecentActivityReceiptSearch_PassesEstateClaimAndRequestToMediator() + { + var user = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim("estateId", "1C8354B7-B97A-46EA-9AD1-C43F33F7E3C3") + }, "Bearer")); + + var request = new TransactionProcessorACL.DataTransferObjects.Requests.RecentActivityReceiptSearchRequest + { + MerchantReportingId = 12345, + ReportDate = new DateTime(2026, 7, 8), + SearchText = "abc", + PageNumber = 2, + PageSize = 5 + }; + + ReportingQueries.GetRecentActivityReceiptSearchQuery? capturedQuery = null; + + var mediator = new Mock(MockBehavior.Strict); + mediator + .Setup(m => m.Send(It.IsAny(), It.IsAny())) + .Callback((payload, _) => capturedQuery = (ReportingQueries.GetRecentActivityReceiptSearchQuery)payload) + .ReturnsAsync(Result.Success(new TransactionProcessorACL.Models.RecentActivityReceiptSearchResponse())); + + var result = await ReportingHandlers.GetRecentActivityReceiptSearch(user, request, mediator.Object, CancellationToken.None); + + result.ShouldNotBeNull(); + capturedQuery.ShouldNotBeNull(); + capturedQuery!.EstateId.ShouldBe(Guid.Parse("1C8354B7-B97A-46EA-9AD1-C43F33F7E3C3")); + capturedQuery.Request.MerchantReportingId.ShouldBe(12345); + capturedQuery.Request.ReportDate.ShouldBe(new DateTime(2026, 7, 8)); + capturedQuery.Request.SearchText.ShouldBe("abc"); + capturedQuery.Request.PageNumber.ShouldBe(2); + capturedQuery.Request.PageSize.ShouldBe(5); + mediator.Verify(m => m.Send(It.IsAny(), It.IsAny()), Times.Once); + } } diff --git a/TransactionProcessorACL/Endpoints/ReportingEndpoints.cs b/TransactionProcessorACL/Endpoints/ReportingEndpoints.cs index 968eb4c..6b399ab 100644 --- a/TransactionProcessorACL/Endpoints/ReportingEndpoints.cs +++ b/TransactionProcessorACL/Endpoints/ReportingEndpoints.cs @@ -17,6 +17,8 @@ public static IEndpointRouteBuilder MapReportingEndpoints(this IEndpointRouteBui group.MapPost("dailymerchantprformancesummary", ReportingHandlers.GetMerchantDailyPerformanceSummary); // POST /api/reporting/transactionmixsummary group.MapPost("transactionmixsummary", ReportingHandlers.GetMerchantTransactionMixSummary); + // POST /api/reporting/recentactivityreceiptsearch + group.MapPost("recentactivityreceiptsearch", ReportingHandlers.GetRecentActivityReceiptSearch); return app; } diff --git a/TransactionProcessorACL/Handlers/ReportingHandlers.cs b/TransactionProcessorACL/Handlers/ReportingHandlers.cs index 3ae728d..32be1b6 100644 --- a/TransactionProcessorACL/Handlers/ReportingHandlers.cs +++ b/TransactionProcessorACL/Handlers/ReportingHandlers.cs @@ -41,4 +41,19 @@ public static async Task GetMerchantTransactionMixSummary(ClaimsPrincip Result result = await mediator.Send(query, cancellationToken); return ResponseFactory.FromResult(result, response => response); } + + public static async Task GetRecentActivityReceiptSearch(ClaimsPrincipal user, + RecentActivityReceiptSearchRequest request, + IMediator mediator, + CancellationToken cancellationToken) + { + Result estateIdResult = Helpers.GetRequiredEstateClaim(user); + if (estateIdResult.IsFailed) + return ResponseFactory.FromResult(Result.Forbidden()); + + ReportingQueries.GetRecentActivityReceiptSearchQuery query = new(estateIdResult.Data, request); + Result result = + await mediator.Send(query, cancellationToken); + return ResponseFactory.FromResult(result, response => response); + } }