Skip to content

Commit 767d242

Browse files
Merge pull request #568 from TransactionProcessing/copilot/add-product-performance-report
Add Product Performance Report with percentage validation and dual view modes
2 parents d902d35 + 5f2cd8d commit 767d242

9 files changed

Lines changed: 499 additions & 7 deletions

File tree

EstateManagementUI.BlazorServer/Components/Pages/Reporting/ProductPerformance.razor

Lines changed: 333 additions & 6 deletions
Large diffs are not rendered by default.

EstateManagementUI.BlazorServer/Models/Models.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,3 +223,11 @@ public class MerchantTransactionSummaryModel
223223
public int SuccessfulTransactionCount { get; set; }
224224
public int FailedTransactionCount { get; set; }
225225
}
226+
227+
public class ProductPerformanceModel
228+
{
229+
public string? ProductName { get; set; }
230+
public int TransactionCount { get; set; }
231+
public decimal TransactionValue { get; set; }
232+
public decimal PercentageContribution { get; set; }
233+
}

EstateManagementUI.BlazorServer/Requests/Requests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public record GetBottomOperatorDataQuery(CorrelationId CorrelationId, string Acc
3939
public record GetLastSettlementQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId) : IRequest<Result<LastSettlementModel>>;
4040
public record GetMerchantQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId) : IRequest<Result<MerchantModel>>;
4141
public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest<Result<List<MerchantTransactionSummaryModel>>>;
42+
public record GetProductPerformanceQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest<Result<List<ProductPerformanceModel>>>;
4243
}
4344

4445
public static class Commands

EstateManagementUI.BlazorServer/Services/TestMediatorService.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, Cancellation
5757
Queries.GetBottomOperatorDataQuery => Task.FromResult((TResponse)(object)Result<List<TopBottomOperatorDataModel>>.Success(GetMockBottomOperators())),
5858
Queries.GetLastSettlementQuery => Task.FromResult((TResponse)(object)Result<LastSettlementModel>.Success(GetMockLastSettlement())),
5959
Queries.GetMerchantTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result<List<MerchantTransactionSummaryModel>>.Success(GetMockMerchantTransactionSummary(query))),
60+
Queries.GetProductPerformanceQuery query => Task.FromResult((TResponse)(object)Result<List<ProductPerformanceModel>>.Success(GetMockProductPerformance(query))),
6061

6162
// Commands - execute against test data store
6263
Commands.CreateMerchantCommand cmd => Task.FromResult((TResponse)(object)ExecuteCreateMerchant(cmd)),
@@ -576,6 +577,74 @@ private List<MerchantTransactionSummaryModel> GetMockMerchantTransactionSummary(
576577
return summary;
577578
}
578579

580+
private List<ProductPerformanceModel> GetMockProductPerformance(Queries.GetProductPerformanceQuery query)
581+
{
582+
var contracts = _testDataStore.GetContracts(query.EstateId);
583+
584+
// Calculate days in the date range to vary data based on period
585+
var daysInRange = (query.EndDate - query.StartDate).Days + 1;
586+
587+
// Use date range as seed for consistent but varying data
588+
var seed = query.StartDate.GetHashCode() ^ query.EndDate.GetHashCode();
589+
var random = new Random(seed);
590+
591+
// Collect all unique products from all contracts
592+
var productNames = contracts
593+
.SelectMany(c => c.Products ?? new List<ContractProductModel>())
594+
.Select(p => p.ProductName)
595+
.Where(p => !string.IsNullOrEmpty(p))
596+
.Distinct()
597+
.ToList();
598+
599+
var products = new List<ProductPerformanceModel>();
600+
decimal totalValue = 0;
601+
602+
// Generate mock transaction data for each product
603+
// Scale transaction counts based on the date range length
604+
var countMultiplier = Math.Max(1, daysInRange / 30.0); // Scale based on 30-day baseline
605+
606+
foreach (var productName in productNames)
607+
{
608+
var baseTransactionCount = random.Next(50, 500);
609+
var transactionCount = (int)(baseTransactionCount * countMultiplier);
610+
var transactionValue = Math.Round((decimal)(random.NextDouble() * 30000 + 5000) * (decimal)countMultiplier, 2);
611+
totalValue += transactionValue;
612+
613+
products.Add(new ProductPerformanceModel
614+
{
615+
ProductName = productName,
616+
TransactionCount = transactionCount,
617+
TransactionValue = transactionValue,
618+
PercentageContribution = 0 // Will be calculated after total is known
619+
});
620+
}
621+
622+
// Calculate percentage contributions (ensure they sum to 100%)
623+
if (totalValue > 0)
624+
{
625+
decimal percentageSum = 0;
626+
for (int i = 0; i < products.Count; i++)
627+
{
628+
if (i == products.Count - 1)
629+
{
630+
// Last item gets the remainder to ensure exact 100% (protected against negative values)
631+
products[i].PercentageContribution = Math.Max(0, Math.Round(100 - percentageSum, 2));
632+
}
633+
else
634+
{
635+
var percentage = Math.Round((products[i].TransactionValue / totalValue) * 100, 2);
636+
products[i].PercentageContribution = percentage;
637+
percentageSum += percentage;
638+
}
639+
}
640+
}
641+
642+
// Sort by transaction value descending
643+
products = products.OrderByDescending(p => p.TransactionValue).ToList();
644+
645+
return products;
646+
}
647+
579648
private Result ExecuteAddOperatorToEstate(Commands.AddOperatorToEstateCommand cmd)
580649
{
581650
var operator1 = _testDataStore.GetOperator(cmd.EstateId, cmd.OperatorId);

EstateManagementUI.BusinessLogic.Tests/MediatorTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public MediatorTests() {
4444
this.Requests.Add(TestData.GetTopOperatorDataQuery);
4545
this.Requests.Add(TestData.GetBottomOperatorDataQuery);
4646
this.Requests.Add(TestData.GetLastSettlementQuery);
47+
this.Requests.Add(TestData.GetProductPerformanceQuery);
4748

4849
// Commands
4950
this.Requests.Add(TestData.AddNewOperatorCommand);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace EstateManagementUI.BusinessLogic.Models;
4+
5+
[ExcludeFromCodeCoverage]
6+
public class ProductPerformanceModel
7+
{
8+
public string? ProductName { get; set; }
9+
public int TransactionCount { get; set; }
10+
public decimal TransactionValue { get; set; }
11+
public decimal PercentageContribution { get; set; }
12+
}

EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ public class ReportingRequestHandler : IRequestHandler<Queries.GetComparisonDate
2323
IRequestHandler<GetTopOperatorDataQuery, Result<List<TopBottomOperatorDataModel>>>,
2424
IRequestHandler<GetBottomOperatorDataQuery, Result<List<TopBottomOperatorDataModel>>>,
2525
IRequestHandler<GetLastSettlementQuery, Result<LastSettlementModel>>,
26-
IRequestHandler<GetMerchantTransactionSummaryQuery, Result<List<MerchantTransactionSummaryModel>>> {
26+
IRequestHandler<GetMerchantTransactionSummaryQuery, Result<List<MerchantTransactionSummaryModel>>>,
27+
IRequestHandler<GetProductPerformanceQuery, Result<List<ProductPerformanceModel>>> {
2728

2829
private readonly IApiClient ApiClient;
2930
public ReportingRequestHandler(IApiClient apiClient)
@@ -166,4 +167,73 @@ public async Task<Result<List<MerchantTransactionSummaryModel>>> Handle(GetMerch
166167

167168
return Result.Success(summary);
168169
}
170+
171+
public async Task<Result<List<ProductPerformanceModel>>> Handle(GetProductPerformanceQuery request,
172+
CancellationToken cancellationToken) {
173+
// TODO: Replace with actual API call when endpoint is available
174+
// For now, return mock data for testing
175+
var contracts = await this.ApiClient.GetContracts(request.AccessToken, Guid.Empty, request.EstateId, cancellationToken);
176+
177+
if (!contracts.IsSuccess) {
178+
return Result.Failure<List<ProductPerformanceModel>>(contracts.Message);
179+
}
180+
181+
var products = new List<ProductPerformanceModel>();
182+
183+
// Calculate days in the date range to vary data based on period
184+
var daysInRange = (request.EndDate - request.StartDate).Days + 1;
185+
186+
// Use date range as seed for consistent but varying data
187+
var seed = request.StartDate.GetHashCode() ^ request.EndDate.GetHashCode();
188+
var random = new Random(seed);
189+
190+
// Collect all unique products from all contracts
191+
var productNames = contracts.Data
192+
.SelectMany(c => c.Products ?? new List<ContractProductModel>())
193+
.Select(p => p.ProductName)
194+
.Distinct()
195+
.ToList();
196+
197+
decimal totalValue = 0;
198+
199+
// Generate mock transaction data for each product
200+
// Scale transaction counts based on the date range length
201+
var countMultiplier = Math.Max(1, daysInRange / 30.0); // Scale based on 30-day baseline
202+
203+
foreach (var productName in productNames) {
204+
if (string.IsNullOrEmpty(productName)) continue;
205+
206+
var baseTransactionCount = random.Next(50, 500);
207+
var transactionCount = (int)(baseTransactionCount * countMultiplier);
208+
var transactionValue = Math.Round((decimal)(random.NextDouble() * 30000 + 5000) * (decimal)countMultiplier, 2);
209+
totalValue += transactionValue;
210+
211+
products.Add(new ProductPerformanceModel {
212+
ProductName = productName,
213+
TransactionCount = transactionCount,
214+
TransactionValue = transactionValue,
215+
PercentageContribution = 0 // Will be calculated after total is known
216+
});
217+
}
218+
219+
// Calculate percentage contributions (ensure they sum to 100%)
220+
if (totalValue > 0) {
221+
decimal percentageSum = 0;
222+
for (int i = 0; i < products.Count; i++) {
223+
if (i == products.Count - 1) {
224+
// Last item gets the remainder to ensure exact 100% (protected against negative values)
225+
products[i].PercentageContribution = Math.Max(0, Math.Round(100 - percentageSum, 2));
226+
} else {
227+
var percentage = Math.Round((products[i].TransactionValue / totalValue) * 100, 2);
228+
products[i].PercentageContribution = percentage;
229+
percentageSum += percentage;
230+
}
231+
}
232+
}
233+
234+
// Sort by transaction value descending
235+
products = products.OrderByDescending(p => p.TransactionValue).ToList();
236+
237+
return Result.Success(products);
238+
}
169239
}

EstateManagementUI.BusinessLogic/Requests/Queries.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,7 @@ public record GetLastSettlementQuery(CorrelationId CorrelationId, String AccessT
7070
public record GetMerchantQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, Guid MerchantId) : IRequest<Result<MerchantModel>>;
7171

7272
public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest<Result<List<MerchantTransactionSummaryModel>>>;
73+
74+
public record GetProductPerformanceQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest<Result<List<ProductPerformanceModel>>>;
7375
}
7476
}

EstateManagementUI.Testing/TestData.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public static class TestData {
9090

9191
public static Queries.GetLastSettlementQuery GetLastSettlementQuery => new(CorrelationId, AccessToken, EstateId);
9292

93+
public static Queries.GetProductPerformanceQuery GetProductPerformanceQuery => new(CorrelationId, AccessToken, EstateId, DateTime.Now.AddDays(-30), DateTime.Now);
94+
9395
public static Commands.AddMerchantCommand AddNewMerchantCommand => new(CorrelationId, AccessToken, EstateId, CreateMerchantModel(BusinessLogic.Models.SettlementSchedule.Immediate));
9496

9597
public static Commands.UpdateMerchantCommand UpdateMerchantCommand => new(CorrelationId, AccessToken, EstateId, Merchant1Id, new UpdateMerchantModel {

0 commit comments

Comments
 (0)