PR O: convert BalanceDataset and BalanceSheetDataset to records

Both were constructor-initialised value carriers with read-only
consumers. Positional record syntax replaces the class + property +
ctor boilerplate; ViewService callers updated to PascalCase named
args to match the record-generated parameter names.

Rest of item 26 descoped: XmlSerializer needs mutable public
setters (value-equality on mutable data is a footgun) and has no
source generator without changing file format. See
IMPROVEMENT_PLAN.md for the full rationale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-07-03 17:56:29 +02:00
parent 786fafefa5
commit e7ff097397
4 changed files with 28 additions and 61 deletions

View file

@ -110,3 +110,4 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full
- **PR L** — Phase 2 item 10: drop the hand-rolled `Copy(target)` methods from `Account`, `BookingRule`, `BookingText`, `SubClass`, and `SplitPredefinition`; keep only a `Transaction.Clone()` for defensive-copy needs in `GetTransaction` / `ViewService.WithDisplaySign`. Every `SaveXxx` now uses `FindIndex` → in-place replace (or `Add` for new entries), moving the two "hidden" defaults (`Account.Currency = "CHF"` when unset, `Transaction.BookingDate = ValueDate` when unset) into the corresponding `SaveXxx` method where they belong. Fixes a pre-existing bug: `Transaction.Copy` never copied `RelatedParty`, so update-saves silently dropped it. Added `PreserveRelatedPartyWhenRoundTrippingTransaction` and `DefaultBookingDateToValueDateWhenBookingDateIsUnset` as regression tests. 42 tests total. - **PR L** — Phase 2 item 10: drop the hand-rolled `Copy(target)` methods from `Account`, `BookingRule`, `BookingText`, `SubClass`, and `SplitPredefinition`; keep only a `Transaction.Clone()` for defensive-copy needs in `GetTransaction` / `ViewService.WithDisplaySign`. Every `SaveXxx` now uses `FindIndex` → in-place replace (or `Add` for new entries), moving the two "hidden" defaults (`Account.Currency = "CHF"` when unset, `Transaction.BookingDate = ValueDate` when unset) into the corresponding `SaveXxx` method where they belong. Fixes a pre-existing bug: `Transaction.Copy` never copied `RelatedParty`, so update-saves silently dropped it. Added `PreserveRelatedPartyWhenRoundTrippingTransaction` and `DefaultBookingDateToValueDateWhenBookingDateIsUnset` as regression tests. 42 tests total.
- **PR M** — Phase 2 item 18 (final slice): add `ILogger<ViewService>` and log a match-count summary from `MatchOpenBankTransactions` (`"Matched {Matched} of {Total} open bank transactions"`). Completes item 18 — statement imports (PR H), repository save failures (PR J), and match runs now all emit structured logs. Also removes a dead `ISettingsService` injection from `Home.razor.cs` (declared with `[Inject]` but never used anywhere in the file). - **PR M** — Phase 2 item 18 (final slice): add `ILogger<ViewService>` and log a match-count summary from `MatchOpenBankTransactions` (`"Matched {Matched} of {Total} open bank transactions"`). Completes item 18 — statement imports (PR H), repository save failures (PR J), and match runs now all emit structured logs. Also removes a dead `ISettingsService` injection from `Home.razor.cs` (declared with `[Inject]` but never used anywhere in the file).
- **PR N** — Small polish pass covering three long-noted rough edges from the original analysis: `ClassIds` becomes a `static class` (was instantiable); `Home.razor.cs` loses the redundant `loaded` guard in `OnInitializedAsync` (Blazor already runs that lifecycle hook once per component instance); the hardcoded `mandator = "Claudio Schaad"` in `SettingsService` moves to a `DefaultMandator` field on `SettingsDataset`, plumbed through `appsettings.Development.json`. - **PR N** — Small polish pass covering three long-noted rough edges from the original analysis: `ClassIds` becomes a `static class` (was instantiable); `Home.razor.cs` loses the redundant `loaded` guard in `OnInitializedAsync` (Blazor already runs that lifecycle hook once per component instance); the hardcoded `mandator = "Claudio Schaad"` in `SettingsService` moves to a `DefaultMandator` field on `SettingsDataset`, plumbed through `appsettings.Development.json`.
- **PR O** — Phase 5 item 26 (partial): convert `BalanceDataset` and `BalanceSheetDataset` to positional records. Both were constructor-initialised value carriers with read-only usage. The rest of item 26 was descoped: the XML-serialised domain models (`Account`, `Transaction`, ...) need mutable public setters for `XmlSerializer`, and value-equality on mutable data is a footgun (hash changes on mutation); source-generated XML serialisers do not exist without switching file formats. `DataSerie` was already a record; `MessageDataset` has a real mutation method (`Add`) and stays a class; `AccountDataset` / `TransactionDataset` / `BookingRuleDataset` inherit from the mutable domain models and can't cleanly become records without a bigger refactor.

View file

@ -1,23 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Schaad.Accounting.Datasets.Reports namespace Schaad.Accounting.Datasets.Reports
{ {
public class BalanceDataset public record BalanceDataset(
{ List<AccountDataset> ActivaAccountList,
public List<AccountDataset> ActivaAccountList { get; set; } List<AccountDataset> PassivaAccountList,
decimal TotalActivaCHF,
public List<AccountDataset> PassivaAccountList { get; set; } decimal TotalPassivaCHF);
}
public decimal TotalActivaCHF { get; set; }
public decimal TotalPassivaCHF { get; set; }
public BalanceDataset(List<AccountDataset> activaAccountList, List<AccountDataset> passivaAccountList, decimal totalActivaCHF, decimal totalPassivaCHF)
{
ActivaAccountList = activaAccountList;
PassivaAccountList = passivaAccountList;
TotalActivaCHF = totalActivaCHF;
TotalPassivaCHF = totalPassivaCHF;
}
}
}

View file

@ -1,29 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
namespace Schaad.Accounting.Datasets.Reports namespace Schaad.Accounting.Datasets.Reports
{ {
public class BalanceSheetDataset public record BalanceSheetDataset(
{ List<AccountDataset> ActivaAccountList,
public List<AccountDataset> ActivaAccountList { get; } List<AccountDataset> IncomeAccountList,
List<AccountDataset> ExpensesAccountList,
public List<AccountDataset> IncomeAccountList { get; } decimal ProfitCHF,
decimal LossCHF,
public List<AccountDataset> ExpensesAccountList { get; } int Year);
}
public decimal ProfitCHF { get; }
public decimal LossCHF { get; }
public int Year { get; }
public BalanceSheetDataset(List<AccountDataset> activaAccountList, List<AccountDataset> incomeAccountList, List<AccountDataset> expensesAccountList, decimal profitCHF, decimal lossCHF, int year)
{
ActivaAccountList = activaAccountList;
IncomeAccountList = incomeAccountList;
ExpensesAccountList = expensesAccountList;
ProfitCHF = profitCHF;
LossCHF = lossCHF;
Year = year;
}
}
}

View file

@ -96,14 +96,11 @@ namespace Schaad.Accounting.Services
public BalanceDataset GetBalanceView() public BalanceDataset GetBalanceView()
{ {
var accountList = GetAccountViewList(); var accountList = GetAccountViewList();
var balanceView = new BalanceDataset( return new BalanceDataset(
activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), ActivaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(),
passivaAccountList: accountList.Where(m => m.Class == ClassIds.Passiva).ToList(), PassivaAccountList: accountList.Where(m => m.Class == ClassIds.Passiva).ToList(),
totalActivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => m.BalanceCHF)), TotalActivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => m.BalanceCHF)),
totalPassivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Passiva).Sum(m => m.BalanceCHF)) TotalPassivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Passiva).Sum(m => m.BalanceCHF)));
);
return balanceView;
} }
public BalanceSheetDataset GetBalanceSheetView(int year) public BalanceSheetDataset GetBalanceSheetView(int year)
@ -115,13 +112,12 @@ namespace Schaad.Accounting.Services
var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF)); var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF));
var balanceView = new BalanceSheetDataset( var balanceView = new BalanceSheetDataset(
activaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), ActivaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(),
incomeAccountList: accountList.Where(m => m.Class == ClassIds.Income).ToList(), IncomeAccountList: accountList.Where(m => m.Class == ClassIds.Income).ToList(),
expensesAccountList: accountList.Where(m => m.Class == ClassIds.Expenses).ToList(), ExpensesAccountList: accountList.Where(m => m.Class == ClassIds.Expenses).ToList(),
profitCHF: profit, ProfitCHF: profit,
lossCHF: loss, LossCHF: loss,
year: year Year: year);
);
return balanceView; return balanceView;
} }