From d16d6ebc1774f1a9e372585eb6688f8039a2fb82 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:00:22 +0000 Subject: [PATCH 1/5] Initial plan From 35e2744a13b4255a74428f56d2bed2ef1a72e878 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:10:30 +0000 Subject: [PATCH 2/5] Implement Transaction Summary by Operator feature Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../TransactionSummaryOperator.razor | 375 +++++++++++++++++- .../Models/Models.cs | 12 + .../Requests/Requests.cs | 1 + .../Models/OperatorTransactionSummaryModel.cs | 16 + .../ReportingRequestHandler.cs | 46 ++- .../Requests/Queries.cs | 2 + 6 files changed, 445 insertions(+), 7 deletions(-) create mode 100644 EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor index 478669d6..40e3d74f 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor @@ -1,5 +1,13 @@ @page "/reporting/transaction-summary-operator" @rendermode InteractiveServer +@using MediatR +@using Microsoft.AspNetCore.Components.Forms +@using EstateManagementUI.BlazorServer.Requests +@using EstateManagementUI.BlazorServer.Models +@using static EstateManagementUI.BlazorServer.Requests.Queries +@inject IMediator Mediator +@inject NavigationManager Navigation +@inject ILogger Logger Transaction Summary by Operator @@ -8,7 +16,7 @@

Transaction Summary by Operator

-

View transaction summaries grouped by operator

+

View aggregated transaction performance per operator

@@ -18,10 +26,365 @@
- -
-
-

Transaction summary by operator report functionality will be implemented here.

+ @if (isLoading) + { + +
+
-
+ } + else if (!string.IsNullOrEmpty(errorMessage)) + { + +
+
+
+ + + +
+

Error Loading Data

+

@errorMessage

+
+
+
+
+ } + else + { + +
+
+

Filters

+
+
+
+ +
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + + +
+
+ Total Operators + @totalOperators +
+
+ +
+
+ + + +
+
+ Total Transactions + @totalTransactions.ToString("N0") +
+
+ +
+
+ + + +
+
+ Total Value + @totalValue.ToString("C") +
+
+ +
+
+ + + +
+
+ Total Fees Earned + @totalFees.ToString("C") +
+
+
+ + +
+
+

Operator Transaction Summary

+
+
+ @if (summaryData != null && summaryData.Any()) + { +
+ + + + + + + + + + + + + + + @foreach (var item in summaryData) + { + + + + + + + + + + + } + + + + + + + + + + + + + +
Operator NameTransaction CountTransaction ValueFees EarnedAverage ValueSuccessfulFailedFailure Rate
@item.OperatorName@item.TotalTransactionCount.ToString("N0")@item.TotalTransactionValue.ToString("C")@item.TotalFeesEarned.ToString("C")@item.AverageTransactionValue.ToString("C")@item.SuccessfulTransactionCount.ToString("N0")@item.FailedTransactionCount.ToString("N0") + @{ + var failureRate = item.TotalTransactionCount > 0 + ? (item.FailedTransactionCount * 100.0 / item.TotalTransactionCount) + : 0; + var rateClass = failureRate <= 5 ? "text-green-600" : failureRate <= 10 ? "text-yellow-600" : "text-red-600"; + } + @failureRate.ToString("F1")% +
Total@totalTransactions.ToString("N0")@totalValue.ToString("C")@totalFees.ToString("C")@(totalTransactions > 0 ? (totalValue / totalTransactions).ToString("C") : "$0.00")@totalSuccessful.ToString("N0")@totalFailed.ToString("N0") + @{ + var overallFailureRate = totalTransactions > 0 ? (totalFailed * 100.0 / totalTransactions) : 0; + var overallRateClass = overallFailureRate <= 5 ? "text-green-600" : overallFailureRate <= 10 ? "text-yellow-600" : "text-red-600"; + } + @overallFailureRate.ToString("F1")% +
+
+ } + else + { +
+ + + +

No transaction data available for the selected period

+
+ } +
+
+ }
+ +@code { + private bool isLoading = true; + private string? errorMessage; + + // Filter states + private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); + private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now); + private string _selectedMerchantId = ""; + private string _selectedOperatorId = ""; + + // Data + private List? summaryData; + private List? merchants; + private List? operators; + + // KPIs + private int totalOperators = 0; + private int totalTransactions = 0; + private decimal totalValue = 0; + private decimal totalFees = 0; + private int totalSuccessful = 0; + private int totalFailed = 0; + + protected override async Task OnInitializedAsync() + { + await LoadData(); + } + + private async Task LoadData() + { + try + { + isLoading = true; + errorMessage = null; + StateHasChanged(); + + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var accessToken = "stubbed-token"; + + // Load filter options + var merchantsTask = Mediator.Send(new GetMerchantsQuery(correlationId, accessToken, estateId)); + var operatorsTask = Mediator.Send(new GetOperatorsQuery(correlationId, accessToken, estateId)); + + await Task.WhenAll(merchantsTask, operatorsTask); + + if (merchantsTask.Result.IsSuccess) + merchants = merchantsTask.Result.Data; + + if (operatorsTask.Result.IsSuccess) + operators = operatorsTask.Result.Data; + + // Load summary data + await LoadSummaryData(); + } + catch (Exception ex) + { + errorMessage = $"Failed to load data: {ex.Message}"; + Logger.LogError(ex, "Error loading operator transaction summary data"); + } + finally + { + isLoading = false; + StateHasChanged(); + } + } + + private async Task LoadSummaryData() + { + try + { + var correlationId = new CorrelationId(Guid.NewGuid()); + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var accessToken = "stubbed-token"; + + var startDate = _startDate.ToDateTime(TimeOnly.MinValue); + var endDate = _endDate.ToDateTime(TimeOnly.MaxValue); + + Guid? merchantId = string.IsNullOrEmpty(_selectedMerchantId) ? null : Guid.Parse(_selectedMerchantId); + Guid? operatorId = string.IsNullOrEmpty(_selectedOperatorId) ? null : Guid.Parse(_selectedOperatorId); + + var result = await Mediator.Send(new GetOperatorTransactionSummaryQuery( + correlationId, + accessToken, + estateId, + startDate, + endDate, + merchantId, + operatorId + )); + + if (result.IsSuccess && result.Data != null) + { + summaryData = result.Data; + CalculateKPIs(); + } + else + { + errorMessage = result.Message ?? "Failed to load summary data"; + } + } + catch (Exception ex) + { + errorMessage = $"Failed to load summary data: {ex.Message}"; + Logger.LogError(ex, "Error loading summary data"); + } + } + + private void CalculateKPIs() + { + if (summaryData == null || !summaryData.Any()) + { + totalOperators = 0; + totalTransactions = 0; + totalValue = 0; + totalFees = 0; + totalSuccessful = 0; + totalFailed = 0; + return; + } + + totalOperators = summaryData.Count; + totalTransactions = summaryData.Sum(s => s.TotalTransactionCount); + totalValue = summaryData.Sum(s => s.TotalTransactionValue); + totalFees = summaryData.Sum(s => s.TotalFeesEarned); + totalSuccessful = summaryData.Sum(s => s.SuccessfulTransactionCount); + totalFailed = summaryData.Sum(s => s.FailedTransactionCount); + } + + private async Task ApplyFilters() + { + await LoadSummaryData(); + } + + private async Task ClearFilters() + { + _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30)); + _endDate = DateOnly.FromDateTime(DateTime.Now); + _selectedMerchantId = ""; + _selectedOperatorId = ""; + await LoadSummaryData(); + } +} + diff --git a/EstateManagementUI.BlazorServer/Models/Models.cs b/EstateManagementUI.BlazorServer/Models/Models.cs index d2cf1af2..40874a65 100644 --- a/EstateManagementUI.BlazorServer/Models/Models.cs +++ b/EstateManagementUI.BlazorServer/Models/Models.cs @@ -224,6 +224,18 @@ public class MerchantTransactionSummaryModel public int FailedTransactionCount { get; set; } } +public class OperatorTransactionSummaryModel +{ + public Guid OperatorId { get; set; } + public string? OperatorName { get; set; } + public int TotalTransactionCount { get; set; } + public decimal TotalTransactionValue { get; set; } + public decimal AverageTransactionValue { get; set; } + public int SuccessfulTransactionCount { get; set; } + public int FailedTransactionCount { get; set; } + public decimal TotalFeesEarned { get; set; } +} + public class ProductPerformanceModel { public string? ProductName { get; set; } diff --git a/EstateManagementUI.BlazorServer/Requests/Requests.cs b/EstateManagementUI.BlazorServer/Requests/Requests.cs index 2f734d3c..e2f3edbf 100644 --- a/EstateManagementUI.BlazorServer/Requests/Requests.cs +++ b/EstateManagementUI.BlazorServer/Requests/Requests.cs @@ -40,6 +40,7 @@ public record GetLastSettlementQuery(CorrelationId CorrelationId, string AccessT public record GetMerchantQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid MerchantId) : IRequest>; public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>>; + public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest>>; } public static class Commands diff --git a/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs new file mode 100644 index 00000000..22023380 --- /dev/null +++ b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; + +namespace EstateManagementUI.BusinessLogic.Models; + +[ExcludeFromCodeCoverage] +public class OperatorTransactionSummaryModel +{ + public Guid OperatorId { get; set; } + public string OperatorName { get; set; } + public int TotalTransactionCount { get; set; } + public decimal TotalTransactionValue { get; set; } + public decimal AverageTransactionValue { get; set; } + public int SuccessfulTransactionCount { get; set; } + public int FailedTransactionCount { get; set; } + public decimal TotalFeesEarned { get; set; } +} diff --git a/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs b/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs index 3736ea87..8230573b 100644 --- a/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs +++ b/EstateManagementUI.BusinessLogic/RequestHandlers/ReportingRequestHandler.cs @@ -24,7 +24,8 @@ public class ReportingRequestHandler : IRequestHandler>>, IRequestHandler>, IRequestHandler>>, -IRequestHandler>> { +IRequestHandler>>, +IRequestHandler>> { private readonly IApiClient ApiClient; public ReportingRequestHandler(IApiClient apiClient) @@ -236,4 +237,47 @@ public async Task>> Handle(GetProductPerfor return Result.Success(products); } + + public async Task>> Handle(GetOperatorTransactionSummaryQuery request, + CancellationToken cancellationToken) { + // TODO: Replace with actual API call when endpoint is available + // For now, return mock data for testing + var operators = await this.ApiClient.GetOperators(request.AccessToken, Guid.Empty, request.EstateId, cancellationToken); + + if (!operators.IsSuccess) { + return Result.Failure>(operators.Message); + } + + var summary = new List(); + var random = new Random(42); // Use seed for consistent test data + + const decimal DefaultSuccessRate = 0.92m; // 92% success rate for mock data + const decimal DefaultFeePercentage = 0.015m; // 1.5% fee rate + + foreach (var op in operators.Data) { + var totalCount = random.Next(500, 5000); + var successfulCount = (int)(totalCount * DefaultSuccessRate); + var failedCount = totalCount - successfulCount; + var totalValue = (decimal)(random.NextDouble() * 500000 + 50000); + var totalFees = Math.Round(totalValue * DefaultFeePercentage, 2); + + summary.Add(new OperatorTransactionSummaryModel { + OperatorId = op.Id, + OperatorName = op.Name, + TotalTransactionCount = totalCount, + TotalTransactionValue = Math.Round(totalValue, 2), + AverageTransactionValue = Math.Round(totalValue / totalCount, 2), + SuccessfulTransactionCount = successfulCount, + FailedTransactionCount = failedCount, + TotalFeesEarned = totalFees + }); + } + + // Apply filters if specified + if (request.OperatorId.HasValue) { + summary = summary.Where(s => s.OperatorId == request.OperatorId.Value).ToList(); + } + + return Result.Success(summary); + } } \ No newline at end of file diff --git a/EstateManagementUI.BusinessLogic/Requests/Queries.cs b/EstateManagementUI.BusinessLogic/Requests/Queries.cs index d54fe9ec..d8cf25b9 100644 --- a/EstateManagementUI.BusinessLogic/Requests/Queries.cs +++ b/EstateManagementUI.BusinessLogic/Requests/Queries.cs @@ -72,5 +72,7 @@ public record GetMerchantQuery(CorrelationId CorrelationId, String AccessToken, public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>>; + + public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, String AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest>>; } } From f613801c260850b78a8f4fe810e016b26ee7e61a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:11:58 +0000 Subject: [PATCH 3/5] Add test mediator support for operator transaction summary Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../Services/TestMediatorService.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs index 2bb982bc..9ebf5b11 100644 --- a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs +++ b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs @@ -58,6 +58,7 @@ public Task Send(IRequest request, Cancellation Queries.GetLastSettlementQuery => Task.FromResult((TResponse)(object)Result.Success(GetMockLastSettlement())), Queries.GetMerchantTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantTransactionSummary(query))), Queries.GetProductPerformanceQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockProductPerformance(query))), + Queries.GetOperatorTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockOperatorTransactionSummary(query))), // Commands - execute against test data store Commands.CreateMerchantCommand cmd => Task.FromResult((TResponse)(object)ExecuteCreateMerchant(cmd)), @@ -666,4 +667,41 @@ private Result ExecuteRemoveOperatorFromEstate(Commands.RemoveOperatorFromEstate // For now, we just return success return Result.Success(); } + + private List GetMockOperatorTransactionSummary(Queries.GetOperatorTransactionSummaryQuery query) + { + var operators = _testDataStore.GetOperators(query.EstateId); + var summary = new List(); + var random = new Random(42); // Use seed for consistent test data + + const decimal DefaultSuccessRate = 0.92m; // 92% success rate for mock data + const decimal DefaultFeePercentage = 0.015m; // 1.5% fee rate + + foreach (var op in operators) + { + // Apply operator filter if specified + if (query.OperatorId.HasValue && op.OperatorId != query.OperatorId.Value) + continue; + + var totalCount = random.Next(500, 5000); + var successfulCount = (int)(totalCount * DefaultSuccessRate); + var failedCount = totalCount - successfulCount; + var totalValue = (decimal)(random.NextDouble() * 500000 + 50000); + var totalFees = Math.Round(totalValue * DefaultFeePercentage, 2); + + summary.Add(new OperatorTransactionSummaryModel + { + OperatorId = op.OperatorId, + OperatorName = op.Name, + TotalTransactionCount = totalCount, + TotalTransactionValue = Math.Round(totalValue, 2), + AverageTransactionValue = Math.Round(totalValue / totalCount, 2), + SuccessfulTransactionCount = successfulCount, + FailedTransactionCount = failedCount, + TotalFeesEarned = totalFees + }); + } + + return summary; + } } From 276b50124488d5cbfd617d1ade5595e9f90b1df2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:14:39 +0000 Subject: [PATCH 4/5] Address code review feedback - fix nullable string and extract helper methods Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../TransactionSummaryOperator.razor | 20 +++++++++++++------ .../Models/OperatorTransactionSummaryModel.cs | 2 +- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor index 40e3d74f..e00128de 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/TransactionSummaryOperator.razor @@ -199,10 +199,8 @@ @item.FailedTransactionCount.ToString("N0") @{ - var failureRate = item.TotalTransactionCount > 0 - ? (item.FailedTransactionCount * 100.0 / item.TotalTransactionCount) - : 0; - var rateClass = failureRate <= 5 ? "text-green-600" : failureRate <= 10 ? "text-yellow-600" : "text-red-600"; + var failureRate = CalculateFailureRate(item.FailedTransactionCount, item.TotalTransactionCount); + var rateClass = GetFailureRateClass(failureRate); } @failureRate.ToString("F1")% @@ -220,8 +218,8 @@ @totalFailed.ToString("N0") @{ - var overallFailureRate = totalTransactions > 0 ? (totalFailed * 100.0 / totalTransactions) : 0; - var overallRateClass = overallFailureRate <= 5 ? "text-green-600" : overallFailureRate <= 10 ? "text-yellow-600" : "text-red-600"; + var overallFailureRate = CalculateFailureRate(totalFailed, totalTransactions); + var overallRateClass = GetFailureRateClass(overallFailureRate); } @overallFailureRate.ToString("F1")% @@ -373,6 +371,16 @@ totalFailed = summaryData.Sum(s => s.FailedTransactionCount); } + private double CalculateFailureRate(int failed, int total) + { + return total > 0 ? (failed * 100.0 / total) : 0; + } + + private string GetFailureRateClass(double failureRate) + { + return failureRate <= 5 ? "text-green-600" : failureRate <= 10 ? "text-yellow-600" : "text-red-600"; + } + private async Task ApplyFilters() { await LoadSummaryData(); diff --git a/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs index 22023380..3524c07b 100644 --- a/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs +++ b/EstateManagementUI.BusinessLogic/Models/OperatorTransactionSummaryModel.cs @@ -6,7 +6,7 @@ namespace EstateManagementUI.BusinessLogic.Models; public class OperatorTransactionSummaryModel { public Guid OperatorId { get; set; } - public string OperatorName { get; set; } + public string? OperatorName { get; set; } public int TotalTransactionCount { get; set; } public decimal TotalTransactionValue { get; set; } public decimal AverageTransactionValue { get; set; } From 805c370d3324b08e0eaf0aab5b427a9ec944e2c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:10:47 +0000 Subject: [PATCH 5/5] Merge main branch to resolve conflicts Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com> --- .../Components/Pages/Merchants/View.razor | 117 ++++++++ .../Components/Pages/Reporting/Index.razor | 8 + .../Reporting/MerchantSettlementHistory.razor | 276 ++++++++++++++++++ .../Models/Models.cs | 9 + .../Requests/Requests.cs | 1 + .../Services/TestMediatorService.cs | 66 +++++ 6 files changed, 477 insertions(+) create mode 100644 EstateManagementUI.BlazorServer/Components/Pages/Reporting/MerchantSettlementHistory.razor diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor index 0100bae7..3452b14d 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Merchants/View.razor @@ -2,6 +2,8 @@ @rendermode InteractiveServer @inject IMediator Mediator @inject NavigationManager NavigationManager +@using EstateManagementUI.BlazorServer.Models +@using EstateManagementUI.BlazorServer.Requests View Merchant @@ -52,6 +54,9 @@ + @@ -226,6 +231,73 @@

Transaction history will be displayed here

} + else if (activeTab == "settlements") + { +
+

Settlement History

+ + @if (loadingSettlements) + { +
+
+
+ } + else if (settlements != null && settlements.Any()) + { +
+
+

Total Settlements

+

@settlements.Count

+
+
+

Total Transactions

+

@settlements.Sum(s => s.TransactionCount).ToString("N0")

+
+
+

Total Paid

+

@settlements.Sum(s => s.NetAmountPaid).ToString("C")

+
+
+ +
+ + + + + + + + + + + @foreach (var settlement in settlements.Take(10)) + { + + + + + + + } + +
DateReferenceTransactionsAmount
@settlement.SettlementDate.ToString("MMM dd, yyyy")@settlement.SettlementReference@settlement.TransactionCount.ToString("N0")@settlement.NetAmountPaid.ToString("C")
+
+ + @if (settlements.Count > 10) + { + + } + } + else + { +

No settlement history available

+ } +
+ } } @@ -250,11 +322,24 @@ private List assignedOperators = new(); private List assignedContracts = new(); private List assignedDevices = new(); + + // Settlement history data + private List? settlements; + private bool loadingSettlements = false; protected override async Task OnInitializedAsync() { await LoadMerchant(); } + + protected override async Task OnParametersSetAsync() + { + // Load settlements when the settlements tab is active + if (activeTab == "settlements" && settlements == null) + { + await LoadSettlements(); + } + } private async Task LoadMerchant() { @@ -299,6 +384,38 @@ isLoading = false; } } + + private async Task LoadSettlements() + { + loadingSettlements = true; + try + { + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); // Test estate ID + var query = new Queries.GetMerchantSettlementHistoryQuery( + CorrelationIdHelper.New(), + "test-token", + estateId, + MerchantId, + DateTime.Today.AddMonths(-6), + DateTime.Today + ); + + var result = await Mediator.Send(query); + if (result.IsSuccess) + { + settlements = result.Data; + } + } + catch (Exception) + { + // Handle error silently for now + settlements = new List(); + } + finally + { + loadingSettlements = false; + } + } private string GetTabClass(string tab) { diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/Index.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/Index.razor index 40bb4505..68afbd96 100644 --- a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/Index.razor +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/Index.razor @@ -82,6 +82,14 @@ + +
+ Merchant Settlement History + + + +
+
diff --git a/EstateManagementUI.BlazorServer/Components/Pages/Reporting/MerchantSettlementHistory.razor b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/MerchantSettlementHistory.razor new file mode 100644 index 00000000..9fafb709 --- /dev/null +++ b/EstateManagementUI.BlazorServer/Components/Pages/Reporting/MerchantSettlementHistory.razor @@ -0,0 +1,276 @@ +@page "/reporting/merchant-settlement-history" +@rendermode InteractiveServer +@inject IMediator Mediator +@inject NavigationManager NavigationManager +@using EstateManagementUI.BlazorServer.Models +@using EstateManagementUI.BlazorServer.Requests + +Merchant Settlement History + +
+ +
+
+

Merchant Settlement History

+

Historical settlement visibility per merchant

+
+ + + + + Back to Reporting + +
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + @if (isLoading) + { +
+
+
+ } + else if (errorMessage != null) + { +
+
+ + + + @errorMessage +
+
+ } + else if (settlements != null && settlements.Any()) + { + +
+
+
+
+
+

Total Settlements

+

@settlements.Count

+
+
+ + + +
+
+
+
+
+
+
+
+

Total Transactions

+

@settlements.Sum(s => s.TransactionCount).ToString("N0")

+
+
+ + + +
+
+
+
+
+
+
+
+

Total Amount Paid

+

@settlements.Sum(s => s.NetAmountPaid).ToString("C")

+
+
+ + + +
+
+
+
+
+
+
+
+

Average Settlement

+

@((settlements.Sum(s => s.NetAmountPaid) / settlements.Count).ToString("C"))

+
+
+ + + +
+
+
+
+
+ + +
+
+

Settlement History

+
+
+
+ + + + + + + + + + + @foreach (var settlement in settlements) + { + + + + + + + } + +
Settlement DateSettlement ReferenceTransaction CountNet Amount Paid
+ @settlement.SettlementDate.ToString("MMM dd, yyyy") + + @settlement.SettlementReference + + @settlement.TransactionCount.ToString("N0") + + @settlement.NetAmountPaid.ToString("C") +
+
+
+
+ } + else if (!isLoading) + { +
+
+ + + +

No settlement history found for the selected criteria

+

Try adjusting your date range or merchant selection

+
+
+ } +
+ +@code { + private List? merchants; + private List? settlements; + private bool isLoading = false; + private string? errorMessage; + + private string selectedMerchantId = ""; + private DateTime startDate = DateTime.Today.AddMonths(-3); + private DateTime endDate = DateTime.Today; + + protected override async Task OnInitializedAsync() + { + await LoadMerchants(); + await LoadSettlementHistory(); + } + + private async Task LoadMerchants() + { + try + { + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); // Test estate ID + var query = new Queries.GetMerchantsQuery( + CorrelationIdHelper.New(), + "test-token", + estateId + ); + + var result = await Mediator.Send(query); + if (result.IsSuccess) + { + merchants = result.Data; + } + } + catch (Exception ex) + { + errorMessage = $"Failed to load merchants: {ex.Message}"; + } + } + + private async Task LoadSettlementHistory() + { + isLoading = true; + errorMessage = null; + + try + { + var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111"); // Test estate ID + Guid? merchantId = string.IsNullOrEmpty(selectedMerchantId) ? null : Guid.Parse(selectedMerchantId); + + var query = new Queries.GetMerchantSettlementHistoryQuery( + CorrelationIdHelper.New(), + "test-token", + estateId, + merchantId, + startDate, + endDate + ); + + var result = await Mediator.Send(query); + if (result.IsSuccess) + { + settlements = result.Data; + } + else + { + errorMessage = result.Message ?? "Failed to load settlement history"; + } + } + catch (Exception ex) + { + errorMessage = $"Error loading settlement history: {ex.Message}"; + } + finally + { + isLoading = false; + } + } +} diff --git a/EstateManagementUI.BlazorServer/Models/Models.cs b/EstateManagementUI.BlazorServer/Models/Models.cs index 40874a65..de66d550 100644 --- a/EstateManagementUI.BlazorServer/Models/Models.cs +++ b/EstateManagementUI.BlazorServer/Models/Models.cs @@ -243,3 +243,12 @@ public class ProductPerformanceModel public decimal TransactionValue { get; set; } public decimal PercentageContribution { get; set; } } + +// Settlement History Models +public class MerchantSettlementHistoryModel +{ + public DateTime SettlementDate { get; set; } + public string? SettlementReference { get; set; } + public int TransactionCount { get; set; } + public decimal NetAmountPaid { get; set; } +} diff --git a/EstateManagementUI.BlazorServer/Requests/Requests.cs b/EstateManagementUI.BlazorServer/Requests/Requests.cs index e2f3edbf..e34c0fee 100644 --- a/EstateManagementUI.BlazorServer/Requests/Requests.cs +++ b/EstateManagementUI.BlazorServer/Requests/Requests.cs @@ -41,6 +41,7 @@ public record GetMerchantQuery(CorrelationId CorrelationId, string AccessToken, public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null, Guid? ProductId = null) : IRequest>>; public record GetProductPerformanceQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest>>; public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest>>; + public record GetMerchantSettlementHistoryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid? MerchantId, DateTime StartDate, DateTime EndDate) : IRequest>>; } public static class Commands diff --git a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs index 9ebf5b11..43cd0773 100644 --- a/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs +++ b/EstateManagementUI.BlazorServer/Services/TestMediatorService.cs @@ -59,6 +59,7 @@ public Task Send(IRequest request, Cancellation Queries.GetMerchantTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantTransactionSummary(query))), Queries.GetProductPerformanceQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockProductPerformance(query))), Queries.GetOperatorTransactionSummaryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockOperatorTransactionSummary(query))), + Queries.GetMerchantSettlementHistoryQuery query => Task.FromResult((TResponse)(object)Result>.Success(GetMockMerchantSettlementHistory(query))), // Commands - execute against test data store Commands.CreateMerchantCommand cmd => Task.FromResult((TResponse)(object)ExecuteCreateMerchant(cmd)), @@ -578,6 +579,71 @@ private List GetMockMerchantTransactionSummary( return summary; } + private List GetMockMerchantSettlementHistory(Queries.GetMerchantSettlementHistoryQuery query) + { + var merchants = _testDataStore.GetMerchants(query.EstateId); + + // Filter by merchant if specified + if (query.MerchantId.HasValue) + { + merchants = merchants.Where(m => m.MerchantId == query.MerchantId.Value).ToList(); + } + + // Generate settlement history data + var settlements = new List(); + var random = new Random(42); // Use seed for consistent data + + // Calculate the number of weeks in the date range + var startDate = query.StartDate.Date; + var endDate = query.EndDate.Date; + var currentDate = startDate; + + // Generate weekly settlements (every Monday) + int settlementCounter = 1; + while (currentDate <= endDate) + { + // Find the next Monday or use current date if it's already Monday + var daysUntilMonday = ((int)DayOfWeek.Monday - (int)currentDate.DayOfWeek + 7) % 7; + if (daysUntilMonday > 0) + { + currentDate = currentDate.AddDays(daysUntilMonday); + } + + if (currentDate > endDate) + break; + + foreach (var merchant in merchants) + { + // Generate unique settlement reference + var settlementRef = $"STL-{currentDate:yyyyMMdd}-{merchant.MerchantReference ?? merchant.MerchantId.ToString().Substring(0, 8)}-{settlementCounter:D4}"; + + // Generate realistic transaction counts and amounts + var transactionCount = random.Next(50, 500); + var averageTransactionValue = (decimal)(random.NextDouble() * 50 + 10); // $10-$60 average + var grossAmount = transactionCount * averageTransactionValue; + var feePercentage = 0.015m + (decimal)(random.NextDouble() * 0.01); // 1.5%-2.5% fee + var fees = grossAmount * feePercentage; + var netAmount = grossAmount - fees; + + settlements.Add(new MerchantSettlementHistoryModel + { + SettlementDate = currentDate, + SettlementReference = settlementRef, + TransactionCount = transactionCount, + NetAmountPaid = Math.Round(netAmount, 2) + }); + + settlementCounter++; + } + + // Move to next week + currentDate = currentDate.AddDays(7); + } + + // Sort by date descending (most recent first) + return settlements.OrderByDescending(s => s.SettlementDate).ToList(); + } + private List GetMockProductPerformance(Queries.GetProductPerformanceQuery query) { var contracts = _testDataStore.GetContracts(query.EstateId);