From 157eaa6a54cd58196a9963e70e66cb25c6c2873a Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Fri, 13 Feb 2026 14:09:40 +0000 Subject: [PATCH] Add Transaction Summary By Merchant report & refactor DTOs Introduced a new "Transaction Summary By Merchant" report with supporting DTOs, MediatR query, endpoint, and business logic for aggregating transaction data by merchant. Refactored and modularized DTOs into separate files. Updated DI and MediatR registrations, fixed a typo, and added comprehensive integration tests for the new feature. Improved code consistency and endpoint naming. --- .../Queries/TransactionQueries.cs | 3 + .../ReportingManager.cs | 116 +++++++++++- .../TransactionRequestHandler.cs | 10 +- .../GroupByOption.cs | 7 + .../SortDirection.cs | 7 + .../SortField.cs | 9 + .../TransactionDetail.cs | 40 ++++ .../TransactionDetailReportRequest.cs | 31 ++++ .../TransactionDetailReportResponse.cs | 53 ++++++ .../TransactionDetailSummary.cs | 14 ++ .../TransactionResult.cs | 121 ------------- .../TransactionSearchRequest.cs | 29 +++ .../ValueRange.cs | 11 ++ .../CalendarEndpointTests.cs | 171 +++++++++++++++++- .../TransactionDetailReportRequest.cs | 35 ++++ .../Bootstrapper/MediatorRegistry.cs | 1 + .../Endpoints/TransactionEndpoints.cs | 4 +- .../Handlers/TransactionHandler.cs | 30 +++ 18 files changed, 566 insertions(+), 126 deletions(-) create mode 100644 EstateReportingAPI.DataTrasferObjects/GroupByOption.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/SortDirection.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/SortField.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs create mode 100644 EstateReportingAPI.DataTrasferObjects/ValueRange.cs diff --git a/EstateReportingAPI.BusinessLogic/Queries/TransactionQueries.cs b/EstateReportingAPI.BusinessLogic/Queries/TransactionQueries.cs index c18b3c0..6956a01 100644 --- a/EstateReportingAPI.BusinessLogic/Queries/TransactionQueries.cs +++ b/EstateReportingAPI.BusinessLogic/Queries/TransactionQueries.cs @@ -10,4 +10,7 @@ public record TransactionQueries { public record TodaysSalesQuery(Guid EstateId, Int32 MerchantReportingId, Int32 OperatorReportingId, DateTime ComparisonDate) : IRequest>; public record TodaysFailedSales(Guid EstateId, DateTime ComparisonDate, String ResponseCode) : IRequest>; public record TransactionDetailReportQuery(Guid EstateId, TransactionDetailReportRequest Request) : IRequest>; + + public record TransactionSummaryByMerchantQuery(Guid EstateId, TransactionSummaryByMerchantRequest Request) : IRequest>; + } \ No newline at end of file diff --git a/EstateReportingAPI.BusinessLogic/ReportingManager.cs b/EstateReportingAPI.BusinessLogic/ReportingManager.cs index 6352e05..4e3457a 100644 --- a/EstateReportingAPI.BusinessLogic/ReportingManager.cs +++ b/EstateReportingAPI.BusinessLogic/ReportingManager.cs @@ -16,6 +16,7 @@ namespace EstateReportingAPI.BusinessLogic; using System.ClientModel.Primitives; using System.Linq; using System.Threading; +using static Microsoft.EntityFrameworkCore.DbLoggerCategory; using Calendar = Models.Calendar; using Merchant = Models.Merchant; @@ -41,9 +42,10 @@ public interface IReportingManager Task>> GetMerchantContracts(MerchantQueries.GetMerchantContractsQuery request, CancellationToken cancellationToken); Task>> GetMerchantDevices(MerchantQueries.GetMerchantDevicesQuery request, CancellationToken cancellationToken); Task> GetOperator(OperatorQueries.GetOperatorQuery request, CancellationToken cancellationToken); - Task> GetTransactionDetailReport(TransactionQueries.TransactionDetailReportQuery request, CancellationToken cancellationToken); + Task> GetTransactionSummaryByMerchantReport(TransactionQueries.TransactionSummaryByMerchantQuery request, + CancellationToken cancellationToken); #endregion } @@ -817,7 +819,7 @@ from s in sJoin.DefaultIfEmpty() query = query.Where(q => request.Request.Operators.Contains(q.OperatorReportingId)); } - var queryResult = await ExecuteQuerySafeToList(query, cancellationToken, "Error retrieving transaction details resport"); + var queryResult = await ExecuteQuerySafeToList(query, cancellationToken, "Error retrieving transaction details report"); if (queryResult.IsFailed) return ResultHelpers.CreateFailure(queryResult); @@ -859,6 +861,116 @@ from s in sJoin.DefaultIfEmpty() } }; + return Result.Success(response); + } + + public async Task> GetTransactionSummaryByMerchantReport(TransactionQueries.TransactionSummaryByMerchantQuery request, + CancellationToken cancellationToken) { + + TransactionSummaryByMerchantResponse response = null; + using ResolvedDbContext? resolvedContext = this.Resolver.Resolve(EstateManagementDatabaseName, request.EstateId.ToString()); + await using EstateManagementContext context = resolvedContext.Context; + + var query = + from t in context.Transactions + join m in context.Merchants on t.MerchantId equals m.MerchantId + join o in context.Operators on t.OperatorId equals o.OperatorId + where t.TransactionType == "Sale" + && t.TransactionDate >= request.Request.StartDate + && t.TransactionDate <= request.Request.EndDate + group t by new + { + t.MerchantId, + m.MerchantReportingId, + MerchantName = m.Name, + t.OperatorId, + o.OperatorReportingId + } + into g + select new + { + g.Key.MerchantId, + g.Key.MerchantReportingId, + g.Key.MerchantName, + g.Key.OperatorId, + g.Key.OperatorReportingId, + TotalCount = g.Count(), + TotalValue = g.Sum(x => x.TransactionAmount), + AuthorisedCount = g.Sum(x => x.IsAuthorised ? 1 : 0), + DeclinedCount = g.Sum(x => x.IsAuthorised ? 0 : 1) + }; + + // Now apply the filters + if (request.Request.Merchants != null && request.Request.Merchants.Any()) + { + query = query.Where(q => request.Request.Merchants.Contains(q.MerchantReportingId)); + } + if (request.Request.Operators != null && request.Request.Operators.Any()) + { + query = query.Where(q => request.Request.Operators.Contains(q.OperatorReportingId)); + } + + var finalQuery = from x in query + group x by new + { + x.MerchantId, + x.MerchantReportingId, + MerchantName = x.MerchantName, + } + into g + select new + { + g.Key.MerchantId, + g.Key.MerchantReportingId, + g.Key.MerchantName, + TotalCount = g.Sum(x => x.TotalCount), + TotalValue = g.Sum(x => x.TotalValue), + AverageValue = g.Count() > 0 ? g.Sum(x => x.TotalValue) / g.Count() : 0m, + AuthorisedCount = g.Sum(x => x.AuthorisedCount), + DeclinedCount = g.Sum(x => x.DeclinedCount), + AuthorisedPercentage = g.Sum(x => x.TotalCount) > 0 ? (decimal)g.Sum(x => x.AuthorisedCount) / (decimal)g.Sum(x => x.TotalCount) : 0m + }; + + var queryResult = await ExecuteQuerySafeToList(finalQuery, cancellationToken, "Error retrieving transaction summary by merchant report"); + + if (queryResult.IsFailed) + return ResultHelpers.CreateFailure(queryResult); + + // Ok now enumerate the results + var queryResults = queryResult.Data; + + if (queryResults.Any() == false) + return new TransactionSummaryByMerchantResponse + { + Summary = new MerchantDetailSummary(), + Merchants= new List() + }; + + // Now to translate the results + response = new TransactionSummaryByMerchantResponse + { + Merchants = queryResults.Select(q => new MerchantDetail + { + MerchantId = q.MerchantId, + MerchantReportingId = q.MerchantReportingId, + MerchantName = q.MerchantName, + AuthorisedCount = q.AuthorisedCount, + AuthorisedPercentage = q.AuthorisedPercentage, + AverageValue = q.AverageValue, + DeclinedCount = q.DeclinedCount, + TotalCount = q.TotalCount, + TotalValue = q.TotalValue + }).ToList(), + Summary = new MerchantDetailSummary + { + TotalCount = queryResults.Sum(q => q.TotalCount), + TotalValue = queryResults.Sum(q => q.TotalValue), + AverageValue = this.SafeDivide(queryResults.Sum(q => q.TotalValue) + ,queryResults.Sum(q => q.TotalCount)), + TotalMerchants = queryResults.Count() + } + }; + return response; } diff --git a/EstateReportingAPI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs b/EstateReportingAPI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs index 82fa12d..85eef6e 100644 --- a/EstateReportingAPI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs +++ b/EstateReportingAPI.BusinessLogic/RequestHandlers/TransactionRequestHandler.cs @@ -7,7 +7,9 @@ namespace EstateReportingAPI.BusinessLogic.RequestHandlers; public class TransactionRequestHandler : IRequestHandler>, IRequestHandler>, -IRequestHandler> { +IRequestHandler>, + IRequestHandler> +{ private readonly IReportingManager Manager; public TransactionRequestHandler(IReportingManager manager) { @@ -28,4 +30,10 @@ public async Task> Handle(TransactionQue CancellationToken cancellationToken) { return await this.Manager.GetTransactionDetailReport(request, cancellationToken); } + + public async Task> Handle(TransactionQueries.TransactionSummaryByMerchantQuery request, + CancellationToken cancellationToken) + { + return await this.Manager.GetTransactionSummaryByMerchantReport(request, cancellationToken); + } } \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs b/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs new file mode 100644 index 0000000..871da51 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/GroupByOption.cs @@ -0,0 +1,7 @@ +namespace EstateReportingAPI.DataTransferObjects; + +public enum GroupByOption{ + Operator, + Merchant, + Product +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/SortDirection.cs b/EstateReportingAPI.DataTrasferObjects/SortDirection.cs new file mode 100644 index 0000000..ecea6f3 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/SortDirection.cs @@ -0,0 +1,7 @@ +namespace EstateReportingAPI.DataTransferObjects; + +public enum SortDirection{ + Ascending, + + Descending, +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/SortField.cs b/EstateReportingAPI.DataTrasferObjects/SortField.cs new file mode 100644 index 0000000..0e89422 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/SortField.cs @@ -0,0 +1,9 @@ +namespace EstateReportingAPI.DataTransferObjects; + +public enum SortField{ + TransactionAmount = 1, + + MerchantName = 2, + + OperatorName = 3 +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs new file mode 100644 index 0000000..f1b039c --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetail.cs @@ -0,0 +1,40 @@ +using System; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class TransactionDetail +{ + [JsonProperty("id")] + public Guid Id { get; set; } + [JsonProperty("date_time")] + public DateTime DateTime { get; set; } + [JsonProperty("merchant")] + public String Merchant { get; set; } + [JsonProperty("merchant_id")] + public Guid MerchantId { get; set; } + [JsonProperty("merchant_reporting_id")] + public Int32 MerchantReportingId { get; set; } + [JsonProperty("operator")] + public String Operator { get; set; } + [JsonProperty("operator_id")] + public Guid OperatorId { get; set; } + [JsonProperty("operator_reporting_id")] + public Int32 OperatorReportingId { get; set; } + [JsonProperty("product")] + public String Product { get; set; } + [JsonProperty("product_id")] + public Guid ProductId { get; set; } + [JsonProperty("product_reporting_id")] + public Int32 ProductReportingId { get; set; } + [JsonProperty("type")] + public String Type { get; set; } + [JsonProperty("status")] + public String Status { get; set; } + [JsonProperty("value")] + public Decimal Value { get; set; } + [JsonProperty("total_fees")] + public Decimal TotalFees { get; set; } + [JsonProperty("settlement_reference")] + public String SettlementReference { get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs new file mode 100644 index 0000000..3738726 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportRequest.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class TransactionDetailReportRequest +{ + [JsonProperty("operators")] + public List? Operators { get; set; } + [JsonProperty("merchants")] + public List? Merchants { get; set; } + [JsonProperty("products")] + public List? Products { get; set; } + [JsonProperty("start_date")] + public DateTime StartDate { get; set; } + [JsonProperty("end_date")] + public DateTime EndDate { get; set; } +} + +public class TransactionSummaryByMerchantRequest +{ + [JsonProperty("operators")] + public List? Operators { get; set; } + [JsonProperty("merchants")] + public List? Merchants { get; set; } + [JsonProperty("start_date")] + public DateTime StartDate { get; set; } + [JsonProperty("end_date")] + public DateTime EndDate { get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs new file mode 100644 index 0000000..67f88ef --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailReportResponse.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class TransactionDetailReportResponse +{ + [JsonProperty("transactions")] + public List Transactions { get; set; } + [JsonProperty("summary")] + public TransactionDetailSummary Summary { get; set; } +} + +public class TransactionSummaryByMerchantResponse +{ + [JsonProperty("merchants")] + public List Merchants { get; set; } + [JsonProperty("summary")] + public MerchantDetailSummary Summary { get; set; } +} + +public class MerchantDetail { + [JsonProperty("merchant_id")] + public Guid MerchantId { get; set; } + [JsonProperty("merchant_reporting_id")] + public Int32 MerchantReportingId { get; set; } + [JsonProperty("merchant_name")] + public string MerchantName { get; set; } + [JsonProperty("total_count")] + public Int32 TotalCount { get; set; } + [JsonProperty("total_value")] + public Decimal TotalValue { get; set; } + [JsonProperty("average_value")] + public Decimal AverageValue { get; set; } + [JsonProperty("authorised_count")] + public Int32 AuthorisedCount { get; set; } + [JsonProperty("declined_count")] + public Int32 DeclinedCount { get; set; } + [JsonProperty("authorised_percentage")] + public Decimal AuthorisedPercentage { get; set; } +} + +public class MerchantDetailSummary { + [JsonProperty("total_merchants")] + public Int32 TotalMerchants { get; set; } + [JsonProperty("total_count")] + public Int32 TotalCount { get; set; } + [JsonProperty("total_value")] + public Decimal TotalValue { get; set; } + [JsonProperty("average_value")] + public Decimal AverageValue { get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs b/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs new file mode 100644 index 0000000..8dc4ee1 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/TransactionDetailSummary.cs @@ -0,0 +1,14 @@ +using System; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class TransactionDetailSummary +{ + [JsonProperty("total_value")] + public Decimal TotalValue { get; set; } + [JsonProperty("total_fees")] + public Decimal TotalFees { get; set; } + [JsonProperty("transaction_count")] + public Int32 TransactionCount { get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs b/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs index 1d8e916..c518cc1 100644 --- a/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs +++ b/EstateReportingAPI.DataTrasferObjects/TransactionResult.cs @@ -1,24 +1,9 @@ using System; -using System.Collections.Generic; using System.Text; namespace EstateReportingAPI.DataTransferObjects{ using Newtonsoft.Json; - public class TransactionDetailReportRequest - { - [JsonProperty("operators")] - public List? Operators { get; set; } - [JsonProperty("merchants")] - public List? Merchants { get; set; } - [JsonProperty("products")] - public List? Products { get; set; } - [JsonProperty("start_date")] - public DateTime StartDate { get; set; } - [JsonProperty("end_date")] - public DateTime EndDate { get; set; } - } - public class TransactionResult{ [JsonProperty("transaction_amount")] public Decimal TransactionAmount{ get; set; } @@ -49,110 +34,4 @@ public class TransactionResult{ [JsonProperty("merchant_reporting_id")] public Int32 MerchantReportingId{ get; set; } } - - public class TransactionSearchRequest{ - - [JsonProperty("operators")] - public List? Operators{ get; set; } - - [JsonProperty("value_range")] - public ValueRange? ValueRange{ get; set; } - - [JsonProperty("merchants")] - public List? Merchants{ get; set; } - - [JsonProperty("query_date")] - public DateTime QueryDate{ get; set; } - - [JsonProperty("response_code")] - public String? ResponseCode{ get; set; } - - [JsonProperty("auth_code")] - public String? AuthCode{ get; set; } - - [JsonProperty("transaction_number")] - public String? TransactionNumber{ get; set; } - } - - public class ValueRange{ - [JsonProperty("start_value")] - public Decimal StartValue{ get; set; } - [JsonProperty("end_value")] - public Decimal EndValue{ get; set; } - } - - public enum SortField{ - TransactionAmount = 1, - - MerchantName = 2, - - OperatorName = 3 - } - - public enum SortDirection{ - Ascending, - - Descending, - } - - public enum GroupByOption{ - Operator, - Merchant, - Product - } - - public class TransactionDetailReportResponse - { - [JsonProperty("transactions")] - public List Transactions { get; set; } - [JsonProperty("summary")] - public TransactionDetailSummary Summary { get; set; } - } - - public class TransactionDetailSummary - { - [JsonProperty("total_value")] - public Decimal TotalValue { get; set; } - [JsonProperty("total_fees")] - public Decimal TotalFees { get; set; } - [JsonProperty("transaction_count")] - public Int32 TransactionCount { get; set; } - } - - public class TransactionDetail - { - [JsonProperty("id")] - public Guid Id { get; set; } - [JsonProperty("date_time")] - public DateTime DateTime { get; set; } - [JsonProperty("merchant")] - public String Merchant { get; set; } - [JsonProperty("merchant_id")] - public Guid MerchantId { get; set; } - [JsonProperty("merchant_reporting_id")] - public Int32 MerchantReportingId { get; set; } - [JsonProperty("operator")] - public String Operator { get; set; } - [JsonProperty("operator_id")] - public Guid OperatorId { get; set; } - [JsonProperty("operator_reporting_id")] - public Int32 OperatorReportingId { get; set; } - [JsonProperty("product")] - public String Product { get; set; } - [JsonProperty("product_id")] - public Guid ProductId { get; set; } - [JsonProperty("product_reporting_id")] - public Int32 ProductReportingId { get; set; } - [JsonProperty("type")] - public String Type { get; set; } - [JsonProperty("status")] - public String Status { get; set; } - [JsonProperty("value")] - public Decimal Value { get; set; } - [JsonProperty("total_fees")] - public Decimal TotalFees { get; set; } - [JsonProperty("settlement_reference")] - public String SettlementReference { get; set; } - } - } diff --git a/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs b/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs new file mode 100644 index 0000000..ce7a9b0 --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/TransactionSearchRequest.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class TransactionSearchRequest{ + + [JsonProperty("operators")] + public List? Operators{ get; set; } + + [JsonProperty("value_range")] + public ValueRange? ValueRange{ get; set; } + + [JsonProperty("merchants")] + public List? Merchants{ get; set; } + + [JsonProperty("query_date")] + public DateTime QueryDate{ get; set; } + + [JsonProperty("response_code")] + public String? ResponseCode{ get; set; } + + [JsonProperty("auth_code")] + public String? AuthCode{ get; set; } + + [JsonProperty("transaction_number")] + public String? TransactionNumber{ get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.DataTrasferObjects/ValueRange.cs b/EstateReportingAPI.DataTrasferObjects/ValueRange.cs new file mode 100644 index 0000000..619c00b --- /dev/null +++ b/EstateReportingAPI.DataTrasferObjects/ValueRange.cs @@ -0,0 +1,11 @@ +using System; +using Newtonsoft.Json; + +namespace EstateReportingAPI.DataTransferObjects; + +public class ValueRange{ + [JsonProperty("start_value")] + public Decimal StartValue{ get; set; } + [JsonProperty("end_value")] + public Decimal EndValue{ get; set; } +} \ No newline at end of file diff --git a/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs b/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs index 4603a20..40bb2f7 100644 --- a/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs +++ b/EstateReportingAPI.IntegrationTests/CalendarEndpointTests.cs @@ -1,18 +1,21 @@ using EstateReportingAPI.DataTransferObjects; using EstateReportingAPI.DataTrasferObjects; using EstateReportingAPI.Models; +using Io.Cucumber.Messages.Types; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; using Shouldly; using SimpleResults; +using Swashbuckle.AspNetCore.SwaggerGen; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.Contracts; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; using TransactionProcessor.Database.Contexts; using TransactionProcessor.Database.Entities; using Xunit; @@ -29,6 +32,7 @@ using TodaysSales = EstateReportingAPI.DataTransferObjects.TodaysSales; using TransactionDetailReportRequest = EstateReportingAPI.DataTransferObjects.TransactionDetailReportRequest; using TransactionDetailReportResponse = EstateReportingAPI.DataTransferObjects.TransactionDetailReportResponse; +using TransactionSummaryByMerchantResponse = EstateReportingAPI.Models.TransactionSummaryByMerchantResponse; namespace EstateReportingAPI.IntegrationTests { public class CalendarEndpointTests : ControllerTestsBase { @@ -1041,6 +1045,171 @@ public async Task TransactionsEndpoint_TransactionDetailReport_ProductFilter_Tra foundTxn.ShouldNotBeNull(transaction.TransactionId.ToString()); } } + + + [Fact] + public async Task TransactionsEndpoint_TransactionSummaryByMerchantReport_NoFilters_SummaryDataReturned() + { + + var transactions = new List(); + + TransactionProcessor.Database.Entities.Merchant merchant1 = this.merchantsList.SingleOrDefault(m => m.Name == "Test Merchant 1"); + var merchant2 = this.merchantsList.SingleOrDefault(m => m.Name == "Test Merchant 2"); + var merchant3 = this.merchantsList.SingleOrDefault(m => m.Name == "Test Merchant 3"); + var safaricomContract = this.contractList.SingleOrDefault(c => c.contractName == "Safaricom Contract"); + var voucherContract = this.contractList.SingleOrDefault(c => c.contractName == "Healthcare Centre 1 Contract"); + var safaricomProduct = contractProducts.Single(cp => cp.Key == safaricomContract.contractId).Value.First(); + var voucherProduct = contractProducts.Single(cp => cp.Key == voucherContract.contractId).Value.First(); + + + List transactionDates = [ + DateTime.Now.Date, + DateTime.Now.Date.AddDays(-1), + DateTime.Now.Date.AddDays(-2), + DateTime.Now.Date.AddDays(-3) + ]; + + Dictionary<(DateTime, TransactionProcessor.Database.Entities.Merchant), Int32> salesConfig = new Dictionary<(DateTime, TransactionProcessor.Database.Entities.Merchant), Int32>(); + salesConfig.Add((DateTime.Now.Date, merchant1), 15); + salesConfig.Add((DateTime.Now.Date.AddDays(-1), merchant1), 25); + salesConfig.Add((DateTime.Now.Date.AddDays(-2), merchant1), 24); + salesConfig.Add((DateTime.Now.Date.AddDays(-3), merchant1), 19); + salesConfig.Add((DateTime.Now.Date, merchant2), 15); + salesConfig.Add((DateTime.Now.Date.AddDays(-1), merchant2), 25); + salesConfig.Add((DateTime.Now.Date.AddDays(-2), merchant2), 24); + salesConfig.Add((DateTime.Now.Date.AddDays(-3), merchant2), 19); + salesConfig.Add((DateTime.Now.Date, merchant3), 15); + salesConfig.Add((DateTime.Now.Date.AddDays(-1), merchant3), 25); + salesConfig.Add((DateTime.Now.Date.AddDays(-2), merchant3), 24); + salesConfig.Add((DateTime.Now.Date.AddDays(-3), merchant3), 19); + + Dictionary totalCountsByMerchant = new(); + Dictionary authorisedCountsByMerchant = new(); + Dictionary declinedCountsByMerchant = new(); + + foreach (DateTime transactionDate in transactionDates) { + + // Build merchant 1 sales + KeyValuePair<(DateTime, TransactionProcessor.Database.Entities.Merchant), Int32> config = salesConfig.SingleOrDefault(c => c.Key == (transactionDate, merchant1)); + + for (int i = 0; i < config.Value; i++) { + string responseCode = i switch { + var n when n % 4 == 2 => "1009", // change value on every 4rd iteration + _ => "0000" + }; + Transaction transaction = await helper.BuildTransactionX(transactionDate, merchant1.MerchantId, safaricomContract.operatorId, safaricomContract.contractId, safaricomProduct.productId, responseCode, safaricomProduct.productValue); + transactions.Add(transaction); + + if (!totalCountsByMerchant.ContainsKey(merchant1)) { + totalCountsByMerchant.Add(merchant1, 0); + authorisedCountsByMerchant.Add(merchant1, 0); + declinedCountsByMerchant.Add(merchant1, 0); + } + totalCountsByMerchant[merchant1]++; + if (responseCode == "0000") { + authorisedCountsByMerchant[merchant1]++; + } + else { + declinedCountsByMerchant[merchant1]++; + } + } + + config = salesConfig.SingleOrDefault(c => c.Key == (transactionDate, merchant2)); + + for (int i = 0; i < config.Value; i++) + { + string responseCode = i switch + { + var n when n % 4 == 2 => "1009", // change value on every 4rd iteration + _ => "0000" + }; + Transaction transaction = await helper.BuildTransactionX(transactionDate, merchant2.MerchantId, safaricomContract.operatorId, safaricomContract.contractId, safaricomProduct.productId, responseCode, safaricomProduct.productValue); + transactions.Add(transaction); + + if (!totalCountsByMerchant.ContainsKey(merchant2)) + { + totalCountsByMerchant.Add(merchant2, 0); + authorisedCountsByMerchant.Add(merchant2, 0); + declinedCountsByMerchant.Add(merchant2, 0); + } + totalCountsByMerchant[merchant2]++; + if (responseCode == "0000") + { + authorisedCountsByMerchant[merchant2]++; + } + else + { + declinedCountsByMerchant[merchant2]++; + } + } + + config = salesConfig.SingleOrDefault(c => c.Key == (transactionDate, merchant3)); + + for (int i = 0; i < config.Value; i++) + { + string responseCode = i switch + { + var n when n % 4 == 2 => "1009", // change value on every 4rd iteration + _ => "0000" + }; + Transaction transaction = await helper.BuildTransactionX(transactionDate, merchant3.MerchantId, safaricomContract.operatorId, safaricomContract.contractId, safaricomProduct.productId, responseCode, safaricomProduct.productValue); + transactions.Add(transaction); + + if (!totalCountsByMerchant.ContainsKey(merchant3)) + { + totalCountsByMerchant.Add(merchant3, 0); + authorisedCountsByMerchant.Add(merchant3, 0); + declinedCountsByMerchant.Add(merchant3, 0); + } + totalCountsByMerchant[merchant3]++; + if (responseCode == "0000") + { + authorisedCountsByMerchant[merchant3]++; + } + else + { + declinedCountsByMerchant[merchant3]++; + } + } + } + await helper.AddTransactionsX(transactions); + + List orderedDates = transactionDates.OrderBy(x => x).ToList(); + TransactionDetailReportRequest request = new TransactionDetailReportRequest + { + StartDate = orderedDates.First(), + EndDate = orderedDates.Last(), + Merchants = [], + Operators = [], + Products = [] + }; + + String payload = JsonConvert.SerializeObject(request); + + var result = await this.CreateAndSendHttpRequestMessage($"{this.BaseRoute}/transactionsummarybymerchantreport", payload, CancellationToken.None); + result.IsSuccess.ShouldBeTrue(); + DataTransferObjects.TransactionSummaryByMerchantResponse transactionSummaryByMerchantResponse = result.Data; + + transactionSummaryByMerchantResponse.ShouldNotBeNull(); + transactionSummaryByMerchantResponse.Summary.ShouldNotBeNull(); + transactionSummaryByMerchantResponse.Summary.TotalMerchants.ShouldBe(3); + transactionSummaryByMerchantResponse.Summary.TotalCount.ShouldBe(transactions.Count); + transactionSummaryByMerchantResponse.Summary.TotalValue.ShouldBe(transactions.Sum(t => t.TransactionAmount)); + + transactionSummaryByMerchantResponse.Merchants.Count.ShouldBe(3); + var merchants = new[] { merchant1, merchant2, merchant3 }; + foreach (TransactionProcessor.Database.Entities.Merchant mr in merchants) { + var row = transactionSummaryByMerchantResponse.Merchants.SingleOrDefault(m => m.MerchantId == mr.MerchantId); + row.ShouldNotBeNull(); + row.MerchantId.ShouldBe(mr.MerchantId); + row.TotalValue.ShouldBe(transactions.Where(t => t.MerchantId == mr.MerchantId).Sum(t => t.TransactionAmount)); + row.TotalCount.ShouldBe(transactions.Count(t => t.MerchantId == mr.MerchantId)); + row.AuthorisedCount.ShouldBe(authorisedCountsByMerchant[mr]); + row.DeclinedCount.ShouldBe(declinedCountsByMerchant[mr]); + } + } + } } + diff --git a/EstateReportingAPI.Models/TransactionDetailReportRequest.cs b/EstateReportingAPI.Models/TransactionDetailReportRequest.cs index 00e949a..95b678c 100644 --- a/EstateReportingAPI.Models/TransactionDetailReportRequest.cs +++ b/EstateReportingAPI.Models/TransactionDetailReportRequest.cs @@ -37,4 +37,39 @@ public class TransactionDetail { public Decimal Value { get; set; } public Decimal TotalFees { get; set; } public String SettlementReference { get; set; } +} + +public class TransactionSummaryByMerchantRequest +{ + public List? Operators { get; set; } + public List? Merchants { get; set; } + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } +} + +public class TransactionSummaryByMerchantResponse +{ + public List Merchants { get; set; } + public MerchantDetailSummary Summary { get; set; } +} + +public class MerchantDetail +{ + public Guid MerchantId { get; set; } + public Int32 MerchantReportingId { get; set; } + public string MerchantName { get; set; } + public Int32 TotalCount { get; set; } + public Decimal TotalValue { get; set; } + public Decimal AverageValue { get; set; } + public Int32 AuthorisedCount { get; set; } + public Int32 DeclinedCount { get; set; } + public Decimal AuthorisedPercentage { get; set; } +} + +public class MerchantDetailSummary +{ + public Int32 TotalMerchants { get; set; } + public Int32 TotalCount { get; set; } + public Decimal TotalValue { get; set; } + public Decimal AverageValue { get; set; } } \ No newline at end of file diff --git a/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs b/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs index 2166b49..a5259b1 100644 --- a/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs +++ b/EstateReportingAPI/Bootstrapper/MediatorRegistry.cs @@ -22,6 +22,7 @@ public MediatorRegistry() { this.AddSingleton>, TransactionRequestHandler>(); this.AddSingleton>, TransactionRequestHandler>(); this.AddSingleton>, TransactionRequestHandler>(); + this.AddSingleton>, TransactionRequestHandler>(); this.AddSingleton>>, CalendarRequestHandler>(); this.AddSingleton>>, CalendarRequestHandler>(); diff --git a/EstateReportingAPI/Endpoints/TransactionEndpoints.cs b/EstateReportingAPI/Endpoints/TransactionEndpoints.cs index d60f62f..ba9c418 100644 --- a/EstateReportingAPI/Endpoints/TransactionEndpoints.cs +++ b/EstateReportingAPI/Endpoints/TransactionEndpoints.cs @@ -22,7 +22,9 @@ public static void MapTransactionEndpoints(this IEndpointRouteBuilder app) .WithStandardProduces(); group.MapGet("todaysfailedsales", TransactionHandler.TodaysFailedSales) .WithStandardProduces(); - group.MapPost("transactionDetailReport", TransactionHandler.TransactionDetailReport) + group.MapPost("transactiondetailreport", TransactionHandler.TransactionDetailReport) .WithStandardProduces(); + group.MapPost("transactionsummarybymerchantreport", TransactionHandler.TransactionSummaryByMerchantReport) + .WithStandardProduces(); } } \ No newline at end of file diff --git a/EstateReportingAPI/Handlers/TransactionHandler.cs b/EstateReportingAPI/Handlers/TransactionHandler.cs index e001042..5908442 100644 --- a/EstateReportingAPI/Handlers/TransactionHandler.cs +++ b/EstateReportingAPI/Handlers/TransactionHandler.cs @@ -83,4 +83,34 @@ TransactionDetailReportResponse SuccessFactory (Models.TransactionDetailReportRe return ResponseFactory.FromResult(result, SuccessFactory); } + + public static async Task TransactionSummaryByMerchantReport([FromHeader] Guid estateId, + [FromBody] TransactionSummaryByMerchantRequest request, + IMediator mediator, CancellationToken cancellationToken) { + Models.TransactionSummaryByMerchantRequest queryRequest = new() { + Merchants = request.Merchants, + Operators = request.Operators, + StartDate = request.StartDate, + EndDate = request.EndDate + }; + var query = new TransactionQueries.TransactionSummaryByMerchantQuery(estateId, queryRequest); + var result = await mediator.Send(query, cancellationToken); + TransactionSummaryByMerchantResponse SuccessFactory (Models.TransactionSummaryByMerchantResponse r) => + new TransactionSummaryByMerchantResponse { + Summary = new MerchantDetailSummary { TotalMerchants = r.Summary.TotalMerchants, TotalCount = r.Summary.TotalCount, TotalValue = r.Summary.TotalValue, AverageValue = r.Summary.AverageValue }, + Merchants = r.Merchants.Select(m => new MerchantDetail { + MerchantId = m.MerchantId, + MerchantName = m.MerchantName, + MerchantReportingId = m.MerchantReportingId, + TotalCount = m.TotalCount, + TotalValue = m.TotalValue, + AverageValue = m.AverageValue, + AuthorisedCount = m.AuthorisedCount, + DeclinedCount = m.DeclinedCount, + AuthorisedPercentage = m.AuthorisedPercentage + }) + .ToList() + }; + return ResponseFactory.FromResult(result, SuccessFactory); + } } \ No newline at end of file