Skip to content

Commit 4f77ce8

Browse files
Implement Settlement Summary Report with models, queries, and UI
Co-authored-by: StuartFerguson <16325469+StuartFerguson@users.noreply.github.com>
1 parent bc7affb commit 4f77ce8

6 files changed

Lines changed: 431 additions & 7 deletions

File tree

Lines changed: 347 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
@page "/reporting/settlement-summary"
22
@rendermode InteractiveServer
3+
@using MediatR
4+
@using Microsoft.AspNetCore.Components.Forms
5+
@using EstateManagementUI.BlazorServer.Requests
6+
@using EstateManagementUI.BlazorServer.Models
7+
@using static EstateManagementUI.BlazorServer.Requests.Queries
8+
@inject IMediator Mediator
9+
@inject NavigationManager Navigation
10+
@inject ILogger<SettlementSummary> Logger
311

412
<PageTitle>Settlement Summary Report</PageTitle>
513

@@ -8,7 +16,7 @@
816
<div class="flex items-center justify-between">
917
<div>
1018
<h1 class="text-2xl font-bold text-gray-900">Settlement Summary Report</h1>
11-
<p class="text-gray-600 mt-1">View settlement summary information</p>
19+
<p class="text-gray-600 mt-1">View settlement outcomes per merchant with net settlement calculations</p>
1220
</div>
1321
<a href="/reporting" class="btn btn-secondary">
1422
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -18,10 +26,343 @@
1826
</a>
1927
</div>
2028

21-
<!-- Report Content -->
22-
<div class="card">
23-
<div class="card-body">
24-
<p class="text-gray-600">Settlement summary report functionality will be implemented here.</p>
29+
@if (isLoading)
30+
{
31+
<!-- Loading State -->
32+
<div class="flex justify-center items-center py-12">
33+
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-admin-primary"></div>
2534
</div>
26-
</div>
35+
}
36+
else if (!string.IsNullOrEmpty(errorMessage))
37+
{
38+
<!-- Error State -->
39+
<div class="card bg-red-50 border border-red-200">
40+
<div class="card-body">
41+
<div class="flex items-center">
42+
<svg class="w-6 h-6 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
44+
</svg>
45+
<div>
46+
<h3 class="text-red-800 font-semibold">Error Loading Data</h3>
47+
<p class="text-red-700 text-sm mt-1">@errorMessage</p>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
}
53+
else
54+
{
55+
<!-- Filters Card -->
56+
<div class="card">
57+
<div class="card-header">
58+
<h3 class="card-title">Filters</h3>
59+
</div>
60+
<div class="card-body">
61+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
62+
<!-- Date Range -->
63+
<div>
64+
<label class="block text-sm font-medium text-gray-700 mb-2">Start Date</label>
65+
<input type="date" @bind="_startDate" @bind:format="yyyy-MM-dd" class="form-control" />
66+
</div>
67+
<div>
68+
<label class="block text-sm font-medium text-gray-700 mb-2">End Date</label>
69+
<input type="date" @bind="_endDate" @bind:format="yyyy-MM-dd" class="form-control" />
70+
</div>
71+
72+
<!-- Merchant Filter -->
73+
<div>
74+
<label class="block text-sm font-medium text-gray-700 mb-2">Merchant</label>
75+
<select @bind="_selectedMerchantId" class="form-control">
76+
<option value="">All Merchants</option>
77+
@if (merchants != null)
78+
{
79+
@foreach (var merchant in merchants)
80+
{
81+
<option value="@merchant.MerchantId">@merchant.MerchantName</option>
82+
}
83+
}
84+
</select>
85+
</div>
86+
</div>
87+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
88+
<!-- Settlement Status Filter -->
89+
<div>
90+
<label class="block text-sm font-medium text-gray-700 mb-2">Settlement Status</label>
91+
<select @bind="_selectedStatus" class="form-control">
92+
<option value="">All Statuses</option>
93+
<option value="settled">Settled</option>
94+
<option value="pending">Pending</option>
95+
<option value="failed">Failed</option>
96+
</select>
97+
</div>
98+
</div>
99+
<div class="mt-4">
100+
<button @onclick="ApplyFilters" class="btn btn-primary">
101+
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
103+
</svg>
104+
Apply Filters
105+
</button>
106+
<button @onclick="ClearFilters" class="btn btn-secondary ml-2">
107+
Clear Filters
108+
</button>
109+
</div>
110+
</div>
111+
</div>
112+
113+
<!-- Summary KPIs -->
114+
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
115+
<div class="info-box">
116+
<div class="info-box-icon bg-admin-primary">
117+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
118+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
119+
</svg>
120+
</div>
121+
<div class="info-box-content">
122+
<span class="info-box-text">Total Merchants</span>
123+
<span class="info-box-number">@totalMerchants</span>
124+
</div>
125+
</div>
126+
127+
<div class="info-box">
128+
<div class="info-box-icon bg-admin-success">
129+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
131+
</svg>
132+
</div>
133+
<div class="info-box-content">
134+
<span class="info-box-text">Gross Value</span>
135+
<span class="info-box-number">@totalGrossValue.ToString("C")</span>
136+
</div>
137+
</div>
138+
139+
<div class="info-box">
140+
<div class="info-box-icon bg-admin-warning">
141+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
142+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z"></path>
143+
</svg>
144+
</div>
145+
<div class="info-box-content">
146+
<span class="info-box-text">Total Fees</span>
147+
<span class="info-box-number">@totalFees.ToString("C")</span>
148+
</div>
149+
</div>
150+
151+
<div class="info-box">
152+
<div class="info-box-icon bg-admin-info">
153+
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
154+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
155+
</svg>
156+
</div>
157+
<div class="info-box-content">
158+
<span class="info-box-text">Net Settlement</span>
159+
<span class="info-box-number">@totalNetSettlement.ToString("C")</span>
160+
</div>
161+
</div>
162+
</div>
163+
164+
<!-- Data Grid -->
165+
<div class="card">
166+
<div class="card-header">
167+
<h3 class="card-title">Settlement Summary</h3>
168+
</div>
169+
<div class="card-body">
170+
@if (summaryData != null && summaryData.Any())
171+
{
172+
<div class="overflow-x-auto">
173+
<table class="table">
174+
<thead>
175+
<tr>
176+
<th>Merchant Name</th>
177+
<th>Settlement Period</th>
178+
<th class="text-right">Gross Value</th>
179+
<th class="text-right">Total Fees</th>
180+
<th class="text-right">Net Settlement</th>
181+
<th class="text-center">Status</th>
182+
</tr>
183+
</thead>
184+
<tbody>
185+
@foreach (var item in summaryData)
186+
{
187+
<tr>
188+
<td class="font-medium">@item.MerchantName</td>
189+
<td>
190+
<div class="text-sm">
191+
<div>@item.SettlementPeriodStart.ToString("yyyy-MM-dd")</div>
192+
<div class="text-gray-500">to @item.SettlementPeriodEnd.ToString("yyyy-MM-dd")</div>
193+
</div>
194+
</td>
195+
<td class="text-right">@item.GrossTransactionValue.ToString("C")</td>
196+
<td class="text-right text-red-600">-@item.TotalFees.ToString("C")</td>
197+
<td class="text-right font-semibold">@item.NetSettlementAmount.ToString("C")</td>
198+
<td class="text-center">
199+
@{
200+
var statusClass = item.SettlementStatus?.ToLower() switch
201+
{
202+
"settled" => "badge-success",
203+
"pending" => "badge-warning",
204+
"failed" => "badge-danger",
205+
_ => "badge-secondary"
206+
};
207+
}
208+
<span class="badge @statusClass">
209+
@(item.SettlementStatus?.ToUpper() ?? "UNKNOWN")
210+
</span>
211+
</td>
212+
</tr>
213+
}
214+
</tbody>
215+
</table>
216+
</div>
217+
<div class="mt-4 p-4 bg-gray-50 rounded-lg">
218+
<p class="text-sm text-gray-600">
219+
<strong>Note:</strong> Net Settlement Amount is calculated as: Gross Transaction Value - Total Fees.
220+
Settlement status reflects the current state from the settlement engine.
221+
</p>
222+
</div>
223+
}
224+
else
225+
{
226+
<div class="text-center py-8 text-gray-500">
227+
<svg class="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
228+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
229+
</svg>
230+
<p class="text-lg">No settlement data available for the selected period</p>
231+
</div>
232+
}
233+
</div>
234+
</div>
235+
}
27236
</div>
237+
238+
@code {
239+
private bool isLoading = true;
240+
private string? errorMessage;
241+
242+
// Filter states
243+
private DateOnly _startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30));
244+
private DateOnly _endDate = DateOnly.FromDateTime(DateTime.Now);
245+
private string _selectedMerchantId = "";
246+
private string _selectedStatus = "";
247+
248+
// Data
249+
private List<SettlementSummaryModel>? summaryData;
250+
private List<MerchantModel>? merchants;
251+
252+
// KPIs
253+
private int totalMerchants = 0;
254+
private decimal totalGrossValue = 0;
255+
private decimal totalFees = 0;
256+
private decimal totalNetSettlement = 0;
257+
258+
protected override async Task OnInitializedAsync()
259+
{
260+
await LoadData();
261+
}
262+
263+
private async Task LoadData()
264+
{
265+
try
266+
{
267+
isLoading = true;
268+
errorMessage = null;
269+
StateHasChanged();
270+
271+
var correlationId = new CorrelationId(Guid.NewGuid());
272+
var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111");
273+
var accessToken = "stubbed-token";
274+
275+
// Load filter options
276+
var merchantsResult = await Mediator.Send(new GetMerchantsQuery(correlationId, accessToken, estateId));
277+
278+
if (merchantsResult.IsSuccess)
279+
merchants = merchantsResult.Data;
280+
281+
// Load summary data
282+
await LoadSummaryData();
283+
}
284+
catch (Exception ex)
285+
{
286+
errorMessage = $"Failed to load data: {ex.Message}";
287+
Logger.LogError(ex, "Error loading settlement summary data");
288+
}
289+
finally
290+
{
291+
isLoading = false;
292+
StateHasChanged();
293+
}
294+
}
295+
296+
private async Task LoadSummaryData()
297+
{
298+
try
299+
{
300+
var correlationId = new CorrelationId(Guid.NewGuid());
301+
var estateId = Guid.Parse("11111111-1111-1111-1111-111111111111");
302+
var accessToken = "stubbed-token";
303+
304+
var startDate = _startDate.ToDateTime(TimeOnly.MinValue);
305+
var endDate = _endDate.ToDateTime(TimeOnly.MaxValue);
306+
307+
Guid? merchantId = string.IsNullOrEmpty(_selectedMerchantId) ? null : Guid.Parse(_selectedMerchantId);
308+
string? status = string.IsNullOrEmpty(_selectedStatus) ? null : _selectedStatus;
309+
310+
var result = await Mediator.Send(new GetSettlementSummaryQuery(
311+
correlationId,
312+
accessToken,
313+
estateId,
314+
startDate,
315+
endDate,
316+
merchantId,
317+
status
318+
));
319+
320+
if (result.IsSuccess && result.Data != null)
321+
{
322+
summaryData = result.Data;
323+
CalculateKPIs();
324+
}
325+
else
326+
{
327+
errorMessage = result.Message ?? "Failed to load settlement summary data";
328+
}
329+
}
330+
catch (Exception ex)
331+
{
332+
errorMessage = $"Failed to load settlement summary data: {ex.Message}";
333+
Logger.LogError(ex, "Error loading settlement summary data");
334+
}
335+
}
336+
337+
private void CalculateKPIs()
338+
{
339+
if (summaryData == null || !summaryData.Any())
340+
{
341+
totalMerchants = 0;
342+
totalGrossValue = 0;
343+
totalFees = 0;
344+
totalNetSettlement = 0;
345+
return;
346+
}
347+
348+
totalMerchants = summaryData.Count;
349+
totalGrossValue = summaryData.Sum(s => s.GrossTransactionValue);
350+
totalFees = summaryData.Sum(s => s.TotalFees);
351+
totalNetSettlement = summaryData.Sum(s => s.NetSettlementAmount);
352+
}
353+
354+
private async Task ApplyFilters()
355+
{
356+
await LoadSummaryData();
357+
}
358+
359+
private async Task ClearFilters()
360+
{
361+
_startDate = DateOnly.FromDateTime(DateTime.Now.AddDays(-30));
362+
_endDate = DateOnly.FromDateTime(DateTime.Now);
363+
_selectedMerchantId = "";
364+
_selectedStatus = "";
365+
await LoadSummaryData();
366+
}
367+
}
368+

EstateManagementUI.BlazorServer/Models/Models.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,16 @@ public class MerchantSettlementHistoryModel
252252
public int TransactionCount { get; set; }
253253
public decimal NetAmountPaid { get; set; }
254254
}
255+
256+
// Settlement Summary Models
257+
public class SettlementSummaryModel
258+
{
259+
public DateTime SettlementPeriodStart { get; set; }
260+
public DateTime SettlementPeriodEnd { get; set; }
261+
public Guid MerchantId { get; set; }
262+
public string? MerchantName { get; set; }
263+
public decimal GrossTransactionValue { get; set; }
264+
public decimal TotalFees { get; set; }
265+
public decimal NetSettlementAmount { get; set; }
266+
public string? SettlementStatus { get; set; }
267+
}

EstateManagementUI.BlazorServer/Requests/Requests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public record GetMerchantTransactionSummaryQuery(CorrelationId CorrelationId, st
4242
public record GetProductPerformanceQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate) : IRequest<Result<List<ProductPerformanceModel>>>;
4343
public record GetOperatorTransactionSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, Guid? OperatorId = null) : IRequest<Result<List<OperatorTransactionSummaryModel>>>;
4444
public record GetMerchantSettlementHistoryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, Guid? MerchantId, DateTime StartDate, DateTime EndDate) : IRequest<Result<List<MerchantSettlementHistoryModel>>>;
45+
public record GetSettlementSummaryQuery(CorrelationId CorrelationId, string AccessToken, Guid EstateId, DateTime StartDate, DateTime EndDate, Guid? MerchantId = null, string? Status = null) : IRequest<Result<List<SettlementSummaryModel>>>;
4546
}
4647

4748
public static class Commands

0 commit comments

Comments
 (0)