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