Commit graph

11 commits

Author SHA1 Message Date
4cc0888627 Fix booking-text totals tests to match intended target-only filter
These tests queried an Activa account (checking) as accountId, but
the totals chart only ever runs for Expense-class accounts, which
are always the target side of a spend booking. With a realistic
expense-account setup, the existing TargetAccountId filter behaves
correctly: it excludes bookings where the account is the origin
(a negative value can't be shown as a pie slice) rather than netting
them against the target-side booking.
2026-08-07 18:29:20 +02:00
5d6d49ed8b Group booking-text charts by first word instead of full text
Bookings like "Narbonne Car" and "Narbonne Food" should roll up into
one series/slice instead of staying separate, so grouping now uses
the text up to the first space via TransactionDataset.GetTextGroup().
2026-08-07 18:23:22 +02:00
7f284b1032 Add booking-text charts to the transactions page
Adds two charts to the per-account transactions view: a stacked bar
chart of monthly totals and a pie chart of overall totals, both
grouped by booking text, so recurring bookings (rent, subscriptions,
etc.) are visible at a glance. Chart components are keyed on the
account id so switching accounts remounts them instead of reusing a
stale Plotly instance.
2026-07-22 14:39:51 +02:00
a19bdf4558 Fix SpendingsOverTime chart: apply FX conversion and use subclass names
ChartService summed raw transaction Value instead of converting via
GetValue(account.IsFxAccount), so FX expense accounts charted in their
foreign currency labeled as CHF. Subclass series also took their Name
from an arbitrary account in the group instead of the actual subclass
name.
2026-07-22 11:03:19 +02:00
f9084dc074 PR P: full async I/O top to bottom (item 19)
Every repository, service, Blazor page/dialog, and test now uses
async/await. Single atomic diff; the codebase does not compile in
intermediate states.

- BaseRepository: LoadListAsync/LoadAsync/SaveAsync return Task<T>;
  per-file locks use SemaphoreSlim so waiters can await; Save
  serialises to a MemoryStream sync (XmlSerializer has no async
  form), then File.WriteAllBytesAsync + sync File.Move.
- RepositoryCache.GetOrLoadAsync takes a Func<Task<List<T>>>.
- All 7 repository interfaces + implementations async.
- All service interfaces + implementations async (except vendor
  IFxService and stateless IFxConverter / SettingsService).
- Every Blazor OnInitializedAsync switches to await base.
- Test suite fully async, 42 tests pass.

AccountRepository.EnsureAccountsFile keeps two .GetAwaiter().GetResult()
bridges because it runs from the constructor.

Null-render guard follow-up (folded in):
Blazor now renders the component once with fields at their initial
values while OnInitializedAsync awaits — so fields declared `= null!`
are actually null on that first render and things like
`accounts.GroupBy(...)` throw ArgumentNullException. Fixed across
Transactions, BalanceReport, BalanceSheetReport, ProfitLossReport,
DetailReport, Assets, Spendings, SpendingsOverTime, TransactionDialog,
TransactionSplitDialog, and BookingRuleDialog:
- Collection fields initialise to [] so first-render loops are empty.
- Single-object data fields become nullable; the razor wraps
  consumption in `@if (field is null) { <p>Lädt…</p> return; }`.
- <PlotlyChart> guarded behind a null check on config/layout/data so
  Plotly.Blazor's @bind doesn't see nulls.
- Header/footer strings initialise to "" instead of null!.

Architectural hygiene on a single-user local Blazor Server app: the
observed win is one File.ReadAllBytesAsync and one
File.WriteAllBytesAsync per Load/Save, and after PR D each file is
loaded at most once per SignalR circuit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 18:30:44 +02:00
1f1575b0af PR M: log MatchOpenBankTransactions summary; drop dead injection
Add ILogger<ViewService> and log the match-count summary at the end
of MatchOpenBankTransactions: "Matched {Matched} of {Total} open bank
transactions". Completes item 18.

Also drop the ISettingsService [Inject] from Home.razor.cs that was
declared but never referenced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 22:04:12 +02:00
3ffe4578c2 PR L: drop Copy(target) methods in favour of index-based replace
Replace the hand-rolled model.Copy(target) pattern in every SaveXxx
with FindIndex -> in-place replace (or Add on new). Move the two
hidden defaults out of the deleted Copy bodies: Currency = "CHF"
default now lives in AccountRepository.SaveAccount; BookingDate =
ValueDate default now lives in TransactionRepository.SaveTransaction.

Delete Copy from Account, BookingRule, BookingText, SubClass, and
SplitPredefinition. Keep Transaction.Clone() (renamed from Copy, and
now includes RelatedParty) for the defensive copy in GetTransaction
and ViewService.WithDisplaySign.

Fixes a pre-existing bug where Transaction.Copy silently dropped
RelatedParty on every update save.

42 tests total, all passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:57:17 +02:00
857b312f9f PR K: hide IFxService and FixerIo key behind IFxConverter
New IFxConverter.ConvertToChf(amount, fromCurrency) and FxConverter
wrap the vendor IFxService and the FixerIo API key so callers stop
threading the key through every conversion. ViewService drops
IFxService and ISettingsService from its constructor and takes
IFxConverter instead; GetAccountViewList and GetBalanceSheetView no
longer read settingsService.GetSettings() per method.

FxConverterTestShould locks in the target-currency + API-key routing.
40 tests total, all passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:51:44 +02:00
3a770af016 PR J: log repository save failures via ILogger
BaseRepository takes an ILogger via its constructor and logs the
failing file path and exception in Save's catch block before
rethrowing. Every concrete repository takes ILogger<ThisRepo> and
passes it up so failures are categorised by repository type.

Pulls Microsoft.Extensions.Logging.Abstractions into the Db project.
TransactionRepositoryTestShould and AccountRepositoryTestShould pass
NullLogger<T>.Instance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:47:55 +02:00
7287dbd440 PR H: log bank-statement imports via ILogger<FileService>
FileService.ImportAccountStatementFile now emits Information on the
file being processed and on each account's import count, Warning on
mandator-mismatch skips, and Error on vendor-parser failures. UI-facing
MessageDataset behaviour is unchanged.

Pulls Microsoft.Extensions.Logging.Abstractions into the Services
project; FileServiceTestShould uses NullLogger<FileService>.Instance.

Rest of item 18 (repository logging, MatchOpenBankTransactions
summary) tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:30:41 +02:00
8926fb1e97 PR G: test project with xxxTestShould / DoThisWhenThat / Shouldly
Add Schaad.Accounting.Tests (xUnit + NSubstitute + Shouldly, net9.0)
wired into Accounting.sln with a direct Reference to
Schaad.Finance.Api.dll so it flows into the test binary.

Naming convention: file/class is <Subject>TestShould, each test method
is DoThisWhenThat. Reads as a sentence:
"ViewService test should sum activa and passiva totals separately when
getting balance view". All assertions use Shouldly (.ShouldBe,
.ShouldBeNull, .ShouldContain, ...) rather than xUnit Assert.*.

38 tests across seven files:

- ViewServiceTestShould: balance math (activa/passiva totals, per-
  account balance from start balance + credits - debits, FX conversion
  to CHF) and bank-transaction auto-matching (booking-rule text,
  value-matching preference, same-accounts-last-month fallback,
  open-transaction filter).
- TransactionRepositoryTestShould: FX round-trip against a temp XML
  directory, unknown-id -> null, and the mutation-on-read regression
  from PR A (a second Get on the same FX transaction used to divide
  by FxRate again).
- FormattingTestShould: Swiss thousands separator, two-decimal
  rounding, culture independence.
- RepositoryCacheTestShould: loader called once, per-key isolation,
  invalidation forces reload, case-insensitive keys.
- AccountRepositoryTestShould: id assignment, in-place update,
  currency defaulting, delete, bank-account lookup, bank-balance
  update, and constructor re-run against an existing file.
- ChartServiceTestShould: honours settingsService.GetYear() without
  mutating it (locks in PR E), account vs sub-class grouping
  heuristic, skips empty accounts.
- FileServiceTestShould: CSV header + running balance for debit and
  credit lines, ordering by BookingDate/ValueDate/Value, no in-place
  value mutation (locks in PR A).

CLAUDE.md gains a `dotnet test` line. IMPROVEMENT_PLAN.md notes that
this landed as three PRs (G, H, I) and was squashed on request.

Also add `*.DotSettings.user` to .gitignore so Rider's per-user
solution settings don't get accidentally staged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:25:48 +02:00