Skip to content

Commit 28567ca

Browse files
CopilotCopilot
andcommitted
feat: data health and reconciliation polish
- Fix concurrent DbContext operations in DataHealthService.AnalyzeAsync (sequential execution) - dotnet format cleanup across controllers, services, and test files - Navigation links between existing and new reconciliation pages - XML documentation for new public API surface - Feature 125 status updated to Done Refs: #125 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2a2eb3b commit 28567ca

File tree

72 files changed

+2065
-958
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+2065
-958
lines changed

docs/125-data-health-and-statement-reconciliation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Feature 125: Data Health & Statement Reconciliation
2-
> **Status:** Planning
2+
> **Status:** Done
33
> **Priority:** High
44
> **Effort:** Large (2 sub-features, ~12 vertical slices)
55

src/BudgetExperiment.Application/DataHealth/DataHealthService.cs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,18 @@ public DataHealthService(
4848
/// <inheritdoc />
4949
public async Task<DataHealthReportDto> AnalyzeAsync(Guid? accountId, CancellationToken ct)
5050
{
51-
var duplicatesTask = FindDuplicatesAsync(accountId, ct);
52-
var outliersTask = FindOutliersAsync(accountId, ct);
53-
var dateGapsTask = FindDateGapsAsync(accountId, minGapDays: 14, ct);
54-
var uncategorizedTask = GetUncategorizedSummaryAsync(ct);
55-
56-
await Task.WhenAll(duplicatesTask, outliersTask, dateGapsTask, uncategorizedTask);
51+
// Sequential: DbContext is scoped per request and cannot handle concurrent operations.
52+
var duplicates = await FindDuplicatesAsync(accountId, ct);
53+
var outliers = await FindOutliersAsync(accountId, ct);
54+
var dateGaps = await FindDateGapsAsync(accountId, minGapDays: 14, ct);
55+
var uncategorized = await GetUncategorizedSummaryAsync(ct);
5756

5857
return new DataHealthReportDto
5958
{
60-
Duplicates = await duplicatesTask,
61-
Outliers = await outliersTask,
62-
DateGaps = await dateGapsTask,
63-
Uncategorized = await uncategorizedTask,
59+
Duplicates = duplicates,
60+
Outliers = outliers,
61+
DateGaps = dateGaps,
62+
Uncategorized = uncategorized,
6463
};
6564
}
6665

src/BudgetExperiment.Client/Models/CurrencyList.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ public static class CurrencyList
1212
/// <summary>
1313
/// Gets the list of common currencies.
1414
/// </summary>
15-
public static IReadOnlyList<CurrencyOption> Currencies { get; } = new CurrencyOption[]
15+
public static IReadOnlyList<CurrencyOption> Currencies
16+
{
17+
get;
18+
}
19+
20+
= new CurrencyOption[]
1621
{
1722
new("USD", "US Dollar", "$"),
1823
new("EUR", "Euro", "\u20ac"),

src/BudgetExperiment.Client/Models/ReportGridDefinition.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ public sealed class ReportGridDefinition
3131
/// <summary>
3232
/// Gets or sets the breakpoint definitions.
3333
/// </summary>
34-
public Dictionary<string, ReportGridBreakpointDefinition> Breakpoints { get; set; } =
34+
public Dictionary<string, ReportGridBreakpointDefinition> Breakpoints
35+
{
36+
get; set;
37+
}
38+
39+
=
3540
new(StringComparer.OrdinalIgnoreCase)
3641
{
3742
["lg"] = new ReportGridBreakpointDefinition { Columns = DefaultColumnsLg },

src/BudgetExperiment.Client/Models/ReportWidgetDefinition.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ public sealed class ReportWidgetDefinition
1212
/// <summary>
1313
/// Gets or sets the widget identifier.
1414
/// </summary>
15-
public Guid Id { get; set; }
15+
public Guid Id
16+
{
17+
get; set;
18+
}
1619

1720
/// <summary>
1821
/// Gets or sets the widget type.
@@ -27,16 +30,27 @@ public sealed class ReportWidgetDefinition
2730
/// <summary>
2831
/// Gets or sets the grid layouts per breakpoint.
2932
/// </summary>
30-
public Dictionary<string, ReportWidgetLayoutPosition> Layouts { get; set; } =
33+
public Dictionary<string, ReportWidgetLayoutPosition> Layouts
34+
{
35+
get; set;
36+
}
37+
38+
=
3139
new(StringComparer.OrdinalIgnoreCase);
3240

3341
/// <summary>
3442
/// Gets or sets sizing constraints for the widget.
3543
/// </summary>
36-
public ReportWidgetConstraints? Constraints { get; set; }
44+
public ReportWidgetConstraints? Constraints
45+
{
46+
get; set;
47+
}
3748

3849
/// <summary>
3950
/// Gets or sets the widget configuration.
4051
/// </summary>
41-
public ReportWidgetConfigDefinition? Config { get; set; }
52+
public ReportWidgetConfigDefinition? Config
53+
{
54+
get; set;
55+
}
4256
}

src/BudgetExperiment.Client/Pages/DataHealth/DataHealthViewModel.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,28 @@ public DataHealthViewModel(IBudgetApiService api)
2525
}
2626

2727
/// <summary>Gets or sets the callback to notify the Razor page that state has changed.</summary>
28-
public Action? OnStateChanged { get; set; }
28+
public Action? OnStateChanged
29+
{
30+
get; set;
31+
}
2932

3033
/// <summary>Gets a value indicating whether data is currently loading.</summary>
31-
public bool IsLoading { get; private set; }
34+
public bool IsLoading
35+
{
36+
get; private set;
37+
}
3238

3339
/// <summary>Gets the current error message, if any.</summary>
34-
public string? ErrorMessage { get; private set; }
40+
public string? ErrorMessage
41+
{
42+
get; private set;
43+
}
3544

3645
/// <summary>Gets the full data health report.</summary>
37-
public DataHealthReportDto? Report { get; private set; }
46+
public DataHealthReportDto? Report
47+
{
48+
get; private set;
49+
}
3850

3951
/// <summary>Gets the count of duplicate clusters.</summary>
4052
public int DuplicateCount => Report?.Duplicates.Count ?? 0;

src/BudgetExperiment.Client/Pages/Reconciliation.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
<PageTitle>Reconciliation - Budget Experiment</PageTitle>
1313

1414
<div class="page-container">
15+
<div class="alert alert-info mb-3" role="alert">
16+
<Icon Name="info" Size="16" /> Looking for bank statement reconciliation?
17+
<a href="/statement-reconciliation" class="alert-link">Go to Statement Reconciliation</a>
18+
</div>
19+
1520
<PageHeader Title="Reconciliation">
1621
<Actions>
1722
<button class="btn btn-secondary" @onclick="ToggleSettings" disabled="@isLoading">

src/BudgetExperiment.Client/Pages/StatementReconciliation/ReconciliationDetailViewModel.cs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,31 @@ public ReconciliationDetailViewModel(IBudgetApiService api)
2525
}
2626

2727
/// <summary>Gets or sets the callback to notify the Razor page that state has changed.</summary>
28-
public Action? OnStateChanged { get; set; }
28+
public Action? OnStateChanged
29+
{
30+
get; set;
31+
}
2932

3033
/// <summary>Gets a value indicating whether data is loading.</summary>
3134
public bool IsLoading { get; private set; } = true;
3235

3336
/// <summary>Gets the current error message, if any.</summary>
34-
public string? ErrorMessage { get; private set; }
37+
public string? ErrorMessage
38+
{
39+
get; private set;
40+
}
3541

3642
/// <summary>Gets the reconciliation record identifier.</summary>
37-
public Guid RecordId { get; private set; }
43+
public Guid RecordId
44+
{
45+
get; private set;
46+
}
3847

3948
/// <summary>Gets the reconciliation history records (all, so we can find the specific one).</summary>
40-
public ReconciliationRecordDto? Record { get; private set; }
49+
public ReconciliationRecordDto? Record
50+
{
51+
get; private set;
52+
}
4153

4254
/// <summary>Gets the transactions locked to this reconciliation record.</summary>
4355
public IReadOnlyList<TransactionDto> Transactions { get; private set; } = [];

src/BudgetExperiment.Client/Pages/StatementReconciliation/ReconciliationHistoryViewModel.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,28 @@ public ReconciliationHistoryViewModel(IBudgetApiService api)
2525
}
2626

2727
/// <summary>Gets or sets the callback to notify the Razor page that state has changed.</summary>
28-
public Action? OnStateChanged { get; set; }
28+
public Action? OnStateChanged
29+
{
30+
get; set;
31+
}
2932

3033
/// <summary>Gets a value indicating whether data is loading.</summary>
3134
public bool IsLoading { get; private set; } = true;
3235

3336
/// <summary>Gets the current error message, if any.</summary>
34-
public string? ErrorMessage { get; private set; }
37+
public string? ErrorMessage
38+
{
39+
get; private set;
40+
}
3541

3642
/// <summary>Gets the available accounts.</summary>
3743
public IReadOnlyList<AccountDto> Accounts { get; private set; } = [];
3844

3945
/// <summary>Gets or sets the selected account identifier.</summary>
40-
public Guid? SelectedAccountId { get; set; }
46+
public Guid? SelectedAccountId
47+
{
48+
get; set;
49+
}
4150

4251
/// <summary>Gets the current page of reconciliation records.</summary>
4352
public IReadOnlyList<ReconciliationRecordDto> Records { get; private set; } = [];

src/BudgetExperiment.Client/Pages/StatementReconciliation/StatementReconciliationViewModel.cs

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,31 +25,49 @@ public StatementReconciliationViewModel(IBudgetApiService api)
2525
}
2626

2727
/// <summary>Gets or sets the callback to notify the Razor page that state has changed.</summary>
28-
public Action? OnStateChanged { get; set; }
28+
public Action? OnStateChanged
29+
{
30+
get; set;
31+
}
2932

3033
/// <summary>Gets a value indicating whether initial data is loading.</summary>
3134
public bool IsLoading { get; private set; } = true;
3235

3336
/// <summary>Gets a value indicating whether an action is in progress.</summary>
34-
public bool IsProcessing { get; private set; }
37+
public bool IsProcessing
38+
{
39+
get; private set;
40+
}
3541

3642
/// <summary>Gets the current error message, if any.</summary>
37-
public string? ErrorMessage { get; private set; }
43+
public string? ErrorMessage
44+
{
45+
get; private set;
46+
}
3847

3948
/// <summary>Gets the list of accounts available for selection.</summary>
4049
public IReadOnlyList<AccountDto> Accounts { get; private set; } = [];
4150

4251
/// <summary>Gets or sets the selected account identifier.</summary>
43-
public Guid? SelectedAccountId { get; set; }
52+
public Guid? SelectedAccountId
53+
{
54+
get; set;
55+
}
4456

4557
/// <summary>Gets or sets the statement closing date.</summary>
4658
public DateOnly StatementDate { get; set; } = DateOnly.FromDateTime(DateTime.UtcNow);
4759

4860
/// <summary>Gets or sets the balance from the bank statement.</summary>
49-
public decimal? StatementBalance { get; set; }
61+
public decimal? StatementBalance
62+
{
63+
get; set;
64+
}
5065

5166
/// <summary>Gets the computed cleared balance (InitialBalance + sum of cleared transactions).</summary>
52-
public decimal ClearedBalance { get; private set; }
67+
public decimal ClearedBalance
68+
{
69+
get; private set;
70+
}
5371

5472
/// <summary>Gets the computed difference between statement balance and cleared balance.</summary>
5573
public decimal Difference => (StatementBalance ?? 0m) - ClearedBalance;
@@ -61,10 +79,16 @@ public StatementReconciliationViewModel(IBudgetApiService api)
6179
public IReadOnlyList<TransactionDto> Transactions { get; private set; } = [];
6280

6381
/// <summary>Gets the active statement balance DTO, if one exists.</summary>
64-
public StatementBalanceDto? ActiveStatementBalance { get; private set; }
82+
public StatementBalanceDto? ActiveStatementBalance
83+
{
84+
get; private set;
85+
}
6586

6687
/// <summary>Gets or sets the last completed reconciliation record, if recently completed.</summary>
67-
public ReconciliationRecordDto? LastCompletedRecord { get; set; }
88+
public ReconciliationRecordDto? LastCompletedRecord
89+
{
90+
get; set;
91+
}
6892

6993
/// <summary>Loads accounts and resets state.</summary>
7094
/// <param name="ct">Cancellation token.</param>

0 commit comments

Comments
 (0)