From f9084dc074b197e128db12ea0097537944a28b82 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Fri, 3 Jul 2026 18:30:44 +0200 Subject: [PATCH] PR P: full async I/O top to bottom (item 19) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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; 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>>. - 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) {

Lädt…

return; }`. - 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 --- IMPROVEMENT_PLAN.md | 3 +- .../Interfaces/IAccountRepository.cs | 19 +++-- .../Interfaces/IBankTransactionRepository.cs | 11 +-- .../Interfaces/IBookingRuleRepository.cs | 14 ++-- .../Interfaces/IBookingTextRepository.cs | 15 ++-- .../ISplitPredefinitionRepository.cs | 5 +- .../Interfaces/ISubclassRepository.cs | 15 ++-- .../Interfaces/ITransactionRepository.cs | 14 ++-- .../Repositories/AccountRepository.cs | 56 +++++-------- .../Repositories/BankTransactionRepository.cs | 23 ++--- .../Repositories/BaseRepository.cs | 72 +++++++++++----- .../Repositories/BookingRuleRepository.cs | 37 +++----- .../Repositories/BookingTextRepository.cs | 37 +++----- .../SplitPredefinitionRepository.cs | 15 ++-- .../Repositories/SubclassRepository.cs | 34 +++----- .../Repositories/TransactionRepository.cs | 45 ++++------ Schaad.Accounting.Db/RepositoryCache.cs | 7 +- Schaad.Accounting.Services/ChartService.cs | 13 +-- Schaad.Accounting.Services/FileService.cs | 18 ++-- .../Interfaces/IChartService.cs | 5 +- .../Interfaces/IFileService.cs | 7 +- .../Interfaces/IViewService.cs | 23 ++--- Schaad.Accounting.Services/ViewService.cs | 81 ++++++++---------- .../AccountRepositoryTestShould.cs | 58 ++++++------- .../ChartServiceTestShould.cs | 46 +++++----- .../FileServiceTestShould.cs | 33 +++----- .../RepositoryCacheTestShould.cs | 24 +++--- .../TransactionRepositoryTestShould.cs | 81 +++++++++--------- .../ViewServiceTestShould.cs | 84 +++++++++---------- .../Components/Pages/Accounts.razor.cs | 38 ++++----- .../Pages/BankTransactions.razor.cs | 14 ++-- .../Components/Pages/BookingRules.razor.cs | 38 ++++----- .../Components/Pages/BookingTexts.razor.cs | 34 ++++---- .../Components/Pages/Charts/Assets.razor | 5 +- .../Components/Pages/Charts/Assets.razor.cs | 14 ++-- .../Components/Pages/Charts/Spendings.razor | 5 +- .../Pages/Charts/Spendings.razor.cs | 14 ++-- .../Pages/Charts/SpendingsOverTime.razor | 5 +- .../Pages/Charts/SpendingsOverTime.razor.cs | 10 +-- .../Components/Pages/Classes.razor.cs | 36 ++++---- .../Pages/Dialogs/AccountDialog.razor.cs | 2 +- .../Pages/Dialogs/BookingRuleDialog.razor | 6 ++ .../Pages/Dialogs/BookingRuleDialog.razor.cs | 8 +- .../Pages/Dialogs/BookingTextDialog.razor.cs | 2 +- .../Pages/Dialogs/ClassDialog.razor.cs | 2 +- .../Pages/Dialogs/TransactionDialog.razor | 6 ++ .../Pages/Dialogs/TransactionDialog.razor.cs | 8 +- .../Dialogs/TransactionSplitDialog.razor | 6 ++ .../Dialogs/TransactionSplitDialog.razor.cs | 16 ++-- .../Components/Pages/Home.razor.cs | 19 ++--- .../Pages/Reports/BalanceReport.razor | 6 ++ .../Pages/Reports/BalanceReport.razor.cs | 14 ++-- .../Pages/Reports/BalanceSheetReport.razor | 6 ++ .../Pages/Reports/BalanceSheetReport.razor.cs | 14 ++-- .../Pages/Reports/DetailReport.razor | 6 ++ .../Pages/Reports/DetailReport.razor.cs | 26 +++--- .../Pages/Reports/ProfitLossReport.razor | 6 ++ .../Pages/Reports/ProfitLossReport.razor.cs | 16 ++-- .../Components/Pages/Transactions.razor | 65 +++++++------- .../Components/Pages/Transactions.razor.cs | 52 ++++++------ 60 files changed, 699 insertions(+), 695 deletions(-) diff --git a/IMPROVEMENT_PLAN.md b/IMPROVEMENT_PLAN.md index 1115fa2..a36bca9 100644 --- a/IMPROVEMENT_PLAN.md +++ b/IMPROVEMENT_PLAN.md @@ -101,7 +101,7 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full - **PR D** — Phase 2 item 9: per-request unit of work / cache. Split off from PR C because it is invasive enough to warrant its own review. - **PR E** — Phase 3 item 22: ChartService cleanup. Promoted ahead of async because it fixes an active correctness bug — `GetAccountExpensesPerMonth(accountId, year)` was mutating `ISettingsService` on the singleton to hop years, with the year parameter always hardcoded to `DateTime.Now.Year`, so the Spendings-over-time chart discarded the user's header year selection. - **PR F** — Phase 3 item 21: precomputed dictionaries in `ViewService` to eliminate O(N·M) `Single(...)` scans and per-account transaction filtering. -- **Item 19 (async I/O)** — deferred. After PR D, each XML file is loaded at most once per SignalR circuit, and this app is single-user local Blazor Server. Converting every repository/service method to async would touch ~40 files for negligible user-visible benefit and real regression risk. Revisit if the app is ever hosted for multiple concurrent users. +- **Item 19 (async I/O)** — landed as PR P after being explicitly requested. See PR P entry below. - **PR G** — Phase 4 items 23 + 24: `Schaad.Accounting.Tests` xUnit + NSubstitute + Shouldly project. Adopts the `xxxTestShould.DoThisWhenThat` naming convention with Shouldly assertions (no xUnit `Assert.*`). Covers `ViewService`, `TransactionRepository`, `Formatting`, `RepositoryCache`, `AccountRepository`, `ChartService`, and `FileService.GetTransactionListCsv` — 38 tests locking in the earlier PRs' behavior. Note: originally shipped as three separate commits (initial project, expanded coverage, Shouldly + naming conversion) and later squashed into one commit at the user's request. - **PR H** — Phase 2 item 18 (first slice): add `ILogger` to `FileService.ImportAccountStatementFile` so bank-statement imports emit `Information` for the file being processed and each account's import count, `Warning` when an account is skipped because it belongs to a different mandator, and `Error` when the vendor parser reports a failure. Rest of the logging (BaseRepository save failures, MatchOpenBankTransactions summary) tracked as a follow-up because it requires threading loggers through all seven repositories. - **PR I** — Phase 2 item 12: move the service interfaces (`IViewService`, `IFileService`, `IChartService`) from `Schaad.Accounting.Common` into `Schaad.Accounting.Services/Interfaces/`. Namespaces are unchanged (`Schaad.Accounting.Interfaces`), so no consumer needs a `using` update. Drops the `Schaad.Finance.Api` `` from `Common.csproj` — Common is now vendor-free and matches its documented role as the "shared models, DTOs, interfaces" layer. @@ -111,3 +111,4 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full - **PR M** — Phase 2 item 18 (final slice): add `ILogger` 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 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. +- **PR P** — Phase 3 item 19: full async I/O top to bottom. `BaseRepository.LoadAsync/SaveAsync/LoadListAsync` return `Task`; the per-file `lock (obj)` becomes `SemaphoreSlim` so it can be `await`-ed. `Save` serialises to a `MemoryStream` synchronously (XmlSerializer has no async form) then writes bytes with `File.WriteAllBytesAsync`; `File.Move` (atomic rename) has no async counterpart in .NET 9 and stays sync. Every repository interface + implementation, every service interface + implementation (except the vendor-owned `IFxService`), every Razor page/dialog `OnInitializedAsync`, and every test-file assertion becomes `async` / `await`. 51 files touched in one atomic diff — the codebase does not compile in intermediate states. `AccountRepository.EnsureAccountsFile` keeps two `.GetAwaiter().GetResult()` bridges because it runs from the constructor (constructors can't be async). The observed async payoff is one `File.ReadAllBytesAsync` and one `File.WriteAllBytesAsync` per Load/Save; after PR D each XML file is deserialised at most once per SignalR circuit, so on this single-user local Blazor Server app this is architectural-hygiene work, not a measurable perf win. diff --git a/Schaad.Accounting.Common/Interfaces/IAccountRepository.cs b/Schaad.Accounting.Common/Interfaces/IAccountRepository.cs index 99caf18..bbc639f 100644 --- a/Schaad.Accounting.Common/Interfaces/IAccountRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/IAccountRepository.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface IAccountRepository { - List GetAccountList(); + Task> GetAccountListAsync(); - void SaveAccount(Account account); + Task SaveAccountAsync(Account account); - Account GetAccount(string id); - - void DeleteAccount(string id); + Task GetAccountAsync(string id); - Account GetAccountForBankAccountNumber(string bankAccountNumber); + Task DeleteAccountAsync(string id); - void SaveBankAccountBalance(string bankAccountNumber, decimal accountBalance); + Task GetAccountForBankAccountNumberAsync(string bankAccountNumber); + + Task SaveBankAccountBalanceAsync(string bankAccountNumber, decimal accountBalance); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Common/Interfaces/IBankTransactionRepository.cs b/Schaad.Accounting.Common/Interfaces/IBankTransactionRepository.cs index 9ec9a5a..9dd8bbd 100644 --- a/Schaad.Accounting.Common/Interfaces/IBankTransactionRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/IBankTransactionRepository.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Datasets; using Schaad.Accounting.Models; @@ -6,10 +7,10 @@ namespace Schaad.Accounting.Interfaces { public interface IBankTransactionRepository { - List GetBankTransactionList(); + Task> GetBankTransactionListAsync(); - int SaveBankTransactionList(string bankAccountNumber, List bankTransactions); + Task SaveBankTransactionListAsync(string bankAccountNumber, List bankTransactions); - BankTransaction GetBankTransaction(string id); + Task GetBankTransactionAsync(string id); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Common/Interfaces/IBookingRuleRepository.cs b/Schaad.Accounting.Common/Interfaces/IBookingRuleRepository.cs index 4893355..dcb0f75 100644 --- a/Schaad.Accounting.Common/Interfaces/IBookingRuleRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/IBookingRuleRepository.cs @@ -1,15 +1,17 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface IBookingRuleRepository { - List GetBookingRuleList(); + Task> GetBookingRuleListAsync(); - void SaveBookingRule(BookingRule bookingRule); + Task SaveBookingRuleAsync(BookingRule bookingRule); - BookingRule GetBookingRule(string id); - void DeleteBookingRule(string id); + Task GetBookingRuleAsync(string id); + + Task DeleteBookingRuleAsync(string id); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Common/Interfaces/IBookingTextRepository.cs b/Schaad.Accounting.Common/Interfaces/IBookingTextRepository.cs index 4ee08fc..2361032 100644 --- a/Schaad.Accounting.Common/Interfaces/IBookingTextRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/IBookingTextRepository.cs @@ -1,16 +1,17 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface IBookingTextRepository { - List GetBookingTextList(); + Task> GetBookingTextListAsync(); - void SaveBookingText(BookingText bookingText); + Task SaveBookingTextAsync(BookingText bookingText); - BookingText GetBookingText(string id); - - void DeleteBookingText(string id); + Task GetBookingTextAsync(string id); + + Task DeleteBookingTextAsync(string id); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Common/Interfaces/ISplitPredefinitionRepository.cs b/Schaad.Accounting.Common/Interfaces/ISplitPredefinitionRepository.cs index 151ec8e..29530f7 100644 --- a/Schaad.Accounting.Common/Interfaces/ISplitPredefinitionRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/ISplitPredefinitionRepository.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface ISplitPredefinitionRepository { - List GetSplitPredefinitionList(); + Task> GetSplitPredefinitionListAsync(); - void SaveSplitPredefinition(SplitPredefinition splitPredefinition); + Task SaveSplitPredefinitionAsync(SplitPredefinition splitPredefinition); } } diff --git a/Schaad.Accounting.Common/Interfaces/ISubclassRepository.cs b/Schaad.Accounting.Common/Interfaces/ISubclassRepository.cs index 70551ed..9e1e52c 100644 --- a/Schaad.Accounting.Common/Interfaces/ISubclassRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/ISubclassRepository.cs @@ -1,20 +1,21 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface ISubclassRepository { - List GetSubClassList(); + Task> GetSubClassListAsync(); - void SaveSubClass(SubClass subClass); + Task SaveSubClassAsync(SubClass subClass); - SubClass GetSubClass(string id); - - void DeleteSubClass(string id); + Task GetSubClassAsync(string id); + + Task DeleteSubClassAsync(string id); Dictionary GetClasses(); string GetClass(int classId); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Common/Interfaces/ITransactionRepository.cs b/Schaad.Accounting.Common/Interfaces/ITransactionRepository.cs index 30cfdc0..9582cb5 100644 --- a/Schaad.Accounting.Common/Interfaces/ITransactionRepository.cs +++ b/Schaad.Accounting.Common/Interfaces/ITransactionRepository.cs @@ -1,15 +1,17 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Models; namespace Schaad.Accounting.Interfaces { public interface ITransactionRepository { - List GetTransactionList(); + Task> GetTransactionListAsync(); - void SaveTransaction(Transaction transaction); + Task SaveTransactionAsync(Transaction transaction); - Transaction GetTransaction(string id); - void DeleteTransaction(string id); + Task GetTransactionAsync(string id); + + Task DeleteTransactionAsync(string id); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/AccountRepository.cs b/Schaad.Accounting.Db/Repositories/AccountRepository.cs index 4c7f032..33a59fa 100644 --- a/Schaad.Accounting.Db/Repositories/AccountRepository.cs +++ b/Schaad.Accounting.Db/Repositories/AccountRepository.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -18,22 +19,16 @@ namespace Schaad.Accounting.Repositories EnsureAccountsFile(); } - /// - /// Load accounts - /// - public List GetAccountList() => LoadList(ACCOUNTS); + public Task> GetAccountListAsync() => LoadListAsync(ACCOUNTS); - /// - /// Save an account (insert/update) - /// - public void SaveAccount(Account account) + public async Task SaveAccountAsync(Account account) { if (string.IsNullOrEmpty(account.Currency)) { account.Currency = "CHF"; } - var accounts = GetAccountList(); + var accounts = await GetAccountListAsync(); var idx = accounts.FindIndex(a => a.Id == account.Id); if (idx >= 0) { @@ -44,42 +39,33 @@ namespace Schaad.Accounting.Repositories account.Id = Guid.NewGuid().ToString(); accounts.Add(account); } - Save(accounts, ACCOUNTS); + await SaveAsync(accounts, ACCOUNTS); } - /// - /// Get account - /// - public Account GetAccount(string id) + public async Task GetAccountAsync(string id) { - var accounts = GetAccountList(); + var accounts = await GetAccountListAsync(); return accounts.FirstOrDefault(t => t.Id == id); } - - /// - /// Delete account - /// - public void DeleteAccount(string id) + + public async Task DeleteAccountAsync(string id) { - var accounts = GetAccountList(); + var accounts = await GetAccountListAsync(); accounts.RemoveAll(t => t.Id == id); - Save(accounts, ACCOUNTS); + await SaveAsync(accounts, ACCOUNTS); } - /// - /// Get account - /// - public Account GetAccountForBankAccountNumber(string bankAccountNumber) + public async Task GetAccountForBankAccountNumberAsync(string bankAccountNumber) { - var accounts = GetAccountList(); + var accounts = await GetAccountListAsync(); return accounts.SingleOrDefault(t => t.BankAccountNumber == bankAccountNumber); } - public void SaveBankAccountBalance(string bankAccountNumber, decimal accountBalance) + public async Task SaveBankAccountBalanceAsync(string bankAccountNumber, decimal accountBalance) { - var account = GetAccountForBankAccountNumber(bankAccountNumber); + var account = await GetAccountForBankAccountNumberAsync(bankAccountNumber); account.LastBankBalance = accountBalance; - SaveAccount(account); + await SaveAccountAsync(account); } private void EnsureAccountsFile() @@ -98,7 +84,9 @@ namespace Schaad.Accounting.Repositories // Year rollover: seed start balances from the previous year's closing values. // Bank accounts start at the last known bank balance; cash accounts start at zero. - var accounts = GetAccountList(); + // Uses the sync GetAwaiter().GetResult() bridge because this runs from the + // constructor; the file was just copied so the underlying I/O is local. + var accounts = GetAccountListAsync().GetAwaiter().GetResult(); var modified = false; foreach (var account in accounts.Where(a => a.Class == ClassIds.Activa)) { @@ -115,8 +103,8 @@ namespace Schaad.Accounting.Repositories } if (modified) { - Save(accounts, ACCOUNTS); + SaveAsync(accounts, ACCOUNTS).GetAwaiter().GetResult(); } } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs b/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs index ec3e611..8f6baa0 100644 --- a/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BankTransactionRepository.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Datasets; using Schaad.Accounting.Interfaces; @@ -16,18 +17,15 @@ namespace Schaad.Accounting.Repositories { } - /// - /// Load bank transactions - /// - public List GetBankTransactionList() => LoadList(BANK_TRANSACTIONS); + public Task> GetBankTransactionListAsync() => LoadListAsync(BANK_TRANSACTIONS); /// /// Save new bank transactions, ignore existing ones (no update) /// - public int SaveBankTransactionList(string bankAccountNumber, List bankTransactions) + public async Task SaveBankTransactionListAsync(string bankAccountNumber, List bankTransactions) { var count = 0; - var transactions = GetBankTransactionList(); + var transactions = await GetBankTransactionListAsync(); foreach (var bankTrx in bankTransactions) { var transaction = transactions.FirstOrDefault(t => t.Id == bankTrx.Id); @@ -48,17 +46,14 @@ namespace Schaad.Accounting.Repositories count++; } } - Save(transactions, BANK_TRANSACTIONS); + await SaveAsync(transactions, BANK_TRANSACTIONS); return count; } - /// - /// Get bank transaction - /// - public BankTransaction GetBankTransaction(string id) + public async Task GetBankTransactionAsync(string id) { - var transactions = GetBankTransactionList(); + var transactions = await GetBankTransactionListAsync(); return transactions.FirstOrDefault(t => t.Id == id); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/BaseRepository.cs b/Schaad.Accounting.Db/Repositories/BaseRepository.cs index 63690a0..59e62bc 100644 --- a/Schaad.Accounting.Db/Repositories/BaseRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BaseRepository.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; using Microsoft.Extensions.Logging; @@ -12,9 +14,9 @@ namespace Schaad.Accounting.Repositories { public abstract class BaseRepository { - // One lock per absolute file path so concurrent Save+Save and Save+Load are serialized - // and can't observe a half-written file. - private static readonly ConcurrentDictionary FileLocks = + // One semaphore per absolute file path so concurrent SaveAsync+SaveAsync and + // SaveAsync+LoadAsync are serialized and can't observe a half-written file. + private static readonly ConcurrentDictionary FileLocks = new(StringComparer.OrdinalIgnoreCase); protected readonly ISettingsService settingsService; @@ -28,10 +30,17 @@ namespace Schaad.Accounting.Repositories this.logger = logger; } + /// + /// Copies fileName from last year's directory to the current year's directory when the + /// current year doesn't have it yet. Kept synchronous because it runs from repository + /// constructors; File.Copy has no async equivalent in .NET 9 either way. + /// protected void EnsureFileExists(string fileName) { string filePath = Path.Combine(settingsService.GetDbPath(), fileName); - lock (GetLock(filePath)) + var semaphore = GetLock(filePath); + semaphore.Wait(); + try { if (File.Exists(filePath) == false) { @@ -42,16 +51,20 @@ namespace Schaad.Accounting.Repositories } } } + finally + { + semaphore.Release(); + } } /// /// Load a list, cached per scope. First call reads the XML file; subsequent calls in - /// the same scope return the cached list. invalidates the entry. + /// the same scope return the cached list. invalidates the entry. /// - protected List LoadList(string fileName) + protected Task> LoadListAsync(string fileName) { var filePath = Path.Combine(settingsService.GetDbPath(), fileName); - return cache.GetOrLoad(filePath, () => Load>(fileName) ?? new List()); + return cache.GetOrLoadAsync(filePath, async () => await LoadAsync>(fileName) ?? new List()); } /// @@ -59,12 +72,14 @@ namespace Schaad.Accounting.Repositories /// renames it, so a crash mid-write leaves the previous file intact. Invalidates /// the corresponding cache entry. /// - protected void Save(T obj, string fileName) + protected async Task SaveAsync(T obj, string fileName) { var filePath = Path.Combine(settingsService.GetDbPath(), fileName); var tmpPath = filePath + ".tmp"; - lock (GetLock(filePath)) + var semaphore = GetLock(filePath); + await semaphore.WaitAsync(); + try { var settings = new XmlWriterSettings { @@ -74,12 +89,16 @@ namespace Schaad.Accounting.Repositories try { - using (var writer = XmlWriter.Create(tmpPath, settings)) + // XmlSerializer is synchronous — serialize to memory, then write async. + using var memory = new MemoryStream(); + using (var writer = XmlWriter.Create(memory, settings)) { var serializer = new XmlSerializer(typeof(T)); serializer.Serialize(writer, obj); } + await File.WriteAllBytesAsync(tmpPath, memory.ToArray()); + // File.Move has no async form in .NET 9. File.Move(tmpPath, filePath, overwrite: true); } catch (Exception ex) @@ -92,34 +111,43 @@ namespace Schaad.Accounting.Repositories throw; } } + finally + { + semaphore.Release(); + } cache.Invalidate(filePath); } /// /// Load an object from an xml file (uncached; used for one-off reads such as file - /// existence bootstrapping). Prefer for repository reads. + /// existence bootstrapping). Prefer for repository reads. /// - protected T Load(string fileName) + protected async Task LoadAsync(string fileName) { var filePath = Path.Combine(settingsService.GetDbPath(), fileName); - lock (GetLock(filePath)) + var semaphore = GetLock(filePath); + await semaphore.WaitAsync(); + try { if (File.Exists(filePath) == false) { return default(T); } - using (XmlReader reader = XmlReader.Create(filePath)) - { - var serializer = new XmlSerializer(typeof(T)); - return (T)serializer.Deserialize(reader); - } + var bytes = await File.ReadAllBytesAsync(filePath); + using var memory = new MemoryStream(bytes); + var serializer = new XmlSerializer(typeof(T)); + return (T)serializer.Deserialize(memory); + } + finally + { + semaphore.Release(); } } - private static object GetLock(string filePath) - => FileLocks.GetOrAdd(filePath, _ => new object()); + private static SemaphoreSlim GetLock(string filePath) + => FileLocks.GetOrAdd(filePath, _ => new SemaphoreSlim(1, 1)); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs b/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs index c5b5110..5743677 100644 --- a/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BookingRuleRepository.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -17,17 +18,11 @@ namespace Schaad.Accounting.Repositories EnsureFileExists(BOOKING_RULES); } - /// - /// Load booking rules - /// - public List GetBookingRuleList() => LoadList(BOOKING_RULES); + public Task> GetBookingRuleListAsync() => LoadListAsync(BOOKING_RULES); - /// - /// Save a booking rule (insert/update) - /// - public void SaveBookingRule(BookingRule bookingRule) + public async Task SaveBookingRuleAsync(BookingRule bookingRule) { - var bookingRules = GetBookingRuleList(); + var bookingRules = await GetBookingRuleListAsync(); var idx = bookingRules.FindIndex(r => r.Id == bookingRule.Id); if (idx >= 0) { @@ -38,26 +33,20 @@ namespace Schaad.Accounting.Repositories bookingRule.Id = Guid.NewGuid().ToString(); bookingRules.Add(bookingRule); } - Save(bookingRules, BOOKING_RULES); + await SaveAsync(bookingRules, BOOKING_RULES); } - /// - /// Get booking rule - /// - public BookingRule GetBookingRule(string id) + public async Task GetBookingRuleAsync(string id) { - var bookingRules = GetBookingRuleList(); + var bookingRules = await GetBookingRuleListAsync(); return bookingRules.FirstOrDefault(t => t.Id == id); } - - /// - /// Delete booking rule - /// - public void DeleteBookingRule(string id) + + public async Task DeleteBookingRuleAsync(string id) { - var bookingRules = GetBookingRuleList(); + var bookingRules = await GetBookingRuleListAsync(); bookingRules.RemoveAll(t => t.Id == id); - Save(bookingRules, BOOKING_RULES); + await SaveAsync(bookingRules, BOOKING_RULES); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs b/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs index 7c399fe..53969b9 100644 --- a/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs +++ b/Schaad.Accounting.Db/Repositories/BookingTextRepository.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -17,17 +18,11 @@ namespace Schaad.Accounting.Repositories EnsureFileExists(BOOKING_TEXTS); } - /// - /// Load booking texts - /// - public List GetBookingTextList() => LoadList(BOOKING_TEXTS); + public Task> GetBookingTextListAsync() => LoadListAsync(BOOKING_TEXTS); - /// - /// Save a booking text (insert/update) - /// - public void SaveBookingText(BookingText bookingText) + public async Task SaveBookingTextAsync(BookingText bookingText) { - var bookingTexts = GetBookingTextList(); + var bookingTexts = await GetBookingTextListAsync(); var idx = bookingTexts.FindIndex(t => t.Id == bookingText.Id); if (idx >= 0) { @@ -38,26 +33,20 @@ namespace Schaad.Accounting.Repositories bookingText.Id = Guid.NewGuid().ToString(); bookingTexts.Add(bookingText); } - Save(bookingTexts, BOOKING_TEXTS); + await SaveAsync(bookingTexts, BOOKING_TEXTS); } - /// - /// Get booking text - /// - public BookingText GetBookingText(string id) + public async Task GetBookingTextAsync(string id) { - var bookingTexts = GetBookingTextList(); + var bookingTexts = await GetBookingTextListAsync(); return bookingTexts.FirstOrDefault(t => t.Id == id); } - - /// - /// Delete booking text - /// - public void DeleteBookingText(string id) + + public async Task DeleteBookingTextAsync(string id) { - var bookingTexts = GetBookingTextList(); + var bookingTexts = await GetBookingTextListAsync(); bookingTexts.RemoveAll(t => t.Id == id); - Save(bookingTexts, BOOKING_TEXTS); + await SaveAsync(bookingTexts, BOOKING_TEXTS); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs b/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs index 2c12fec..b8f94ce 100644 --- a/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/SplitPredefinitionRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -17,17 +18,11 @@ namespace Schaad.Accounting.Repositories EnsureFileExists(SPLIT_PREDEFINITION); } - /// - /// Load split predefinitions - /// - public List GetSplitPredefinitionList() => LoadList(SPLIT_PREDEFINITION); + public Task> GetSplitPredefinitionListAsync() => LoadListAsync(SPLIT_PREDEFINITION); - /// - /// Save a split predefinition (insert/update) - /// - public void SaveSplitPredefinition(SplitPredefinition splitPredefinition) + public async Task SaveSplitPredefinitionAsync(SplitPredefinition splitPredefinition) { - var definitions = GetSplitPredefinitionList(); + var definitions = await GetSplitPredefinitionListAsync(); var idx = definitions.FindIndex(d => d.Id == splitPredefinition.Id); if (idx >= 0) { @@ -38,7 +33,7 @@ namespace Schaad.Accounting.Repositories splitPredefinition.Id = Guid.NewGuid().ToString(); definitions.Add(splitPredefinition); } - Save(definitions, SPLIT_PREDEFINITION); + await SaveAsync(definitions, SPLIT_PREDEFINITION); } } } diff --git a/Schaad.Accounting.Db/Repositories/SubclassRepository.cs b/Schaad.Accounting.Db/Repositories/SubclassRepository.cs index f9935c0..b7f5878 100644 --- a/Schaad.Accounting.Db/Repositories/SubclassRepository.cs +++ b/Schaad.Accounting.Db/Repositories/SubclassRepository.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -23,17 +24,11 @@ namespace Schaad.Accounting.Repositories classes.Add(ClassIds.Expenses, "Ausgaben"); } - /// - /// Load subclasses - /// - public List GetSubClassList() => LoadList(SUBCLASSES); + public Task> GetSubClassListAsync() => LoadListAsync(SUBCLASSES); - /// - /// Save a subclass (insert/update) - /// - public void SaveSubClass(SubClass subClass) + public async Task SaveSubClassAsync(SubClass subClass) { - var subclasses = GetSubClassList(); + var subclasses = await GetSubClassListAsync(); var idx = subclasses.FindIndex(s => s.Id == subClass.Id); if (idx >= 0) { @@ -44,23 +39,20 @@ namespace Schaad.Accounting.Repositories subClass.Id = Guid.NewGuid().ToString(); subclasses.Add(subClass); } - Save(subclasses, SUBCLASSES); + await SaveAsync(subclasses, SUBCLASSES); } - /// - /// Get subclass - /// - public SubClass GetSubClass(string id) + public async Task GetSubClassAsync(string id) { - var subclasses = GetSubClassList(); + var subclasses = await GetSubClassListAsync(); return subclasses.FirstOrDefault(t => t.Id == id); } - - public void DeleteSubClass(string id) + + public async Task DeleteSubClassAsync(string id) { - var subclasses = GetSubClassList(); + var subclasses = await GetSubClassListAsync(); subclasses.RemoveAll(t => t.Id == id); - Save(subclasses, SUBCLASSES); + await SaveAsync(subclasses, SUBCLASSES); } public Dictionary GetClasses() @@ -73,4 +65,4 @@ namespace Schaad.Accounting.Repositories return classes[classId]; } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/Repositories/TransactionRepository.cs b/Schaad.Accounting.Db/Repositories/TransactionRepository.cs index d7aa5e2..4472517 100644 --- a/Schaad.Accounting.Db/Repositories/TransactionRepository.cs +++ b/Schaad.Accounting.Db/Repositories/TransactionRepository.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -18,20 +19,14 @@ namespace Schaad.Accounting.Repositories this.accountRepository = accountRepository; } - /// - /// Get transaction list - /// - public List GetTransactionList() => LoadList(TRANSACTIONS); + public Task> GetTransactionListAsync() => LoadListAsync(TRANSACTIONS); - /// - /// Save a transaction (insert/update) - /// - public void SaveTransaction(Transaction transaction) + public async Task SaveTransactionAsync(Transaction transaction) { transaction.Value = Math.Abs(transaction.Value); - var originIsFx = accountRepository.GetAccount(transaction.OriginAccountId).IsFxAccount; - var targetIsFx = accountRepository.GetAccount(transaction.TargetAccountId).IsFxAccount; + var originIsFx = (await accountRepository.GetAccountAsync(transaction.OriginAccountId)).IsFxAccount; + var targetIsFx = (await accountRepository.GetAccountAsync(transaction.TargetAccountId)).IsFxAccount; var isFxAccount = originIsFx || targetIsFx; // user enters amount in the foreign account's currency -> convert to CHF for storage @@ -50,7 +45,7 @@ namespace Schaad.Accounting.Repositories transaction.BookingDate = transaction.ValueDate; } - var transactionList = GetTransactionList(); + var transactionList = await GetTransactionListAsync(); var idx = transactionList.FindIndex(t => t.Id == transaction.Id); if (idx >= 0) { @@ -61,15 +56,12 @@ namespace Schaad.Accounting.Repositories transaction.Id = Guid.NewGuid().ToString(); transactionList.Add(transaction); } - Save(transactionList, TRANSACTIONS); + await SaveAsync(transactionList, TRANSACTIONS); } - /// - /// Get transaction - /// - public Transaction GetTransaction(string id) + public async Task GetTransactionAsync(string id) { - var transactions = GetTransactionList(); + var transactions = await GetTransactionListAsync(); var stored = transactions.FirstOrDefault(t => t.Id == id); if (stored == null) { @@ -79,8 +71,8 @@ namespace Schaad.Accounting.Repositories var result = stored.Clone(); // value is stored in CHF -> convert back to foreign currency for display/editing - var isFxAccount = accountRepository.GetAccount(result.OriginAccountId).IsFxAccount - || accountRepository.GetAccount(result.TargetAccountId).IsFxAccount; + var isFxAccount = (await accountRepository.GetAccountAsync(result.OriginAccountId)).IsFxAccount + || (await accountRepository.GetAccountAsync(result.TargetAccountId)).IsFxAccount; if (isFxAccount && result.FxRate != 0) { result.Value = result.Value / result.FxRate.Value; @@ -88,15 +80,12 @@ namespace Schaad.Accounting.Repositories return result; } - - /// - /// Delete transaction - /// - public void DeleteTransaction(string id) + + public async Task DeleteTransactionAsync(string id) { - var transactions = GetTransactionList(); + var transactions = await GetTransactionListAsync(); transactions.RemoveAll(t => t.Id == id); - Save(transactions, TRANSACTIONS); + await SaveAsync(transactions, TRANSACTIONS); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Db/RepositoryCache.cs b/Schaad.Accounting.Db/RepositoryCache.cs index e1834af..00f0f9b 100644 --- a/Schaad.Accounting.Db/RepositoryCache.cs +++ b/Schaad.Accounting.Db/RepositoryCache.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace Schaad.Accounting.Repositories { @@ -7,7 +8,7 @@ namespace Schaad.Accounting.Repositories /// Per-scope cache of deserialized XML lists, keyed by absolute file path. /// /// Registered as Scoped so it lives for the SignalR circuit; each XML file is - /// then loaded at most once per circuit. Writes go through , + /// then loaded at most once per circuit. Writes go through , /// which invalidates the corresponding entry so the next read reloads from disk. /// /// Not thread-safe — callers within a single Blazor Server circuit run serialized on the @@ -18,14 +19,14 @@ namespace Schaad.Accounting.Repositories private readonly Dictionary entries = new(StringComparer.OrdinalIgnoreCase); - public List GetOrLoad(string cacheKey, Func> loader) + public async Task> GetOrLoadAsync(string cacheKey, Func>> loader) { if (entries.TryGetValue(cacheKey, out var cached)) { return (List)cached; } - var list = loader(); + var list = await loader(); entries[cacheKey] = list; return list; } diff --git a/Schaad.Accounting.Services/ChartService.cs b/Schaad.Accounting.Services/ChartService.cs index 169cdd3..ecf51e9 100644 --- a/Schaad.Accounting.Services/ChartService.cs +++ b/Schaad.Accounting.Services/ChartService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets.Charts; using Schaad.Accounting.Interfaces; @@ -18,9 +19,9 @@ namespace Schaad.Accounting.Services this.viewService = viewService; } - public IReadOnlyList GetExpensesPerMonth() + public async Task> GetExpensesPerMonthAsync() { - var expenseTransactions = viewService.GetTransactionViewList() + var expenseTransactions = (await viewService.GetTransactionViewListAsync()) .Where(t => t.TargetAccount.Class == ClassIds.Expenses) .ToList(); @@ -31,7 +32,7 @@ namespace Schaad.Accounting.Services return distinctSubClasses > 1 ? GetSubClassExpensesPerMonth(expenseTransactions) - : GetAccountExpensesPerMonth(); + : await GetAccountExpensesPerMonthAsync(); } private List GetSubClassExpensesPerMonth(List expenseTransactions) @@ -59,11 +60,11 @@ namespace Schaad.Accounting.Services return list; } - private List GetAccountExpensesPerMonth() + private async Task> GetAccountExpensesPerMonthAsync() { var year = settingsService.GetYear(); - var allTransactions = viewService.GetTransactionViewList(); - var expenseAccounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Expenses); + var allTransactions = await viewService.GetTransactionViewListAsync(); + var expenseAccounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses); var list = new List(); foreach (var account in expenseAccounts) diff --git a/Schaad.Accounting.Services/FileService.cs b/Schaad.Accounting.Services/FileService.cs index 4882278..14960ac 100644 --- a/Schaad.Accounting.Services/FileService.cs +++ b/Schaad.Accounting.Services/FileService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO.Compression; using System.Linq; using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Datasets; using Schaad.Accounting.Interfaces; @@ -47,15 +48,14 @@ namespace Schaad.Accounting.Services } // Upload account statement file (mt940, camt053) - // http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6 - public IReadOnlyList ImportAccountStatementFile(string filePath) + public async Task> ImportAccountStatementFileAsync(string filePath) { logger.LogInformation("Importing account statement file {FilePath}", filePath); var messages = new List(); var accountStatementResults = accountStatementService.ReadFile(filePath, Encoding.UTF8); - var accountList = accountRepository.GetAccountList().Select(a => a.BankAccountNumber).ToList(); + var accountList = (await accountRepository.GetAccountListAsync()).Select(a => a.BankAccountNumber).ToList(); foreach (var accountStatementResult in accountStatementResults) { var account = accountStatementResult.AccountStatement; @@ -91,8 +91,8 @@ namespace Schaad.Accounting.Services Creditor = t.Creditor }) .ToList(); - var count = bankTransactionRepository.SaveBankTransactionList(account.AccountNumber, transactionsThisYear); - accountRepository.SaveBankAccountBalance(account.AccountNumber, (decimal)account.EndBalance.Value); + var count = await bankTransactionRepository.SaveBankTransactionListAsync(account.AccountNumber, transactionsThisYear); + await accountRepository.SaveBankAccountBalanceAsync(account.AccountNumber, (decimal)account.EndBalance.Value); logger.LogInformation("Imported {Imported} of {Total} transactions for account {AccountNumber}", count, account.Transactions.Count, account.AccountNumber); @@ -124,11 +124,11 @@ namespace Schaad.Accounting.Services return creditCardTransactions; } - public byte[] GetTransactionListCsv(string accountId) + public async Task GetTransactionListCsvAsync(string accountId) { var sb = new StringBuilder(); - var account = accountRepository.GetAccount(accountId); - var transactions = transactionsRepository.GetTransactionList() + var account = await accountRepository.GetAccountAsync(accountId); + var transactions = (await transactionsRepository.GetTransactionListAsync()) .Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId) .OrderBy(t => t.BookingDate) .ThenBy(t => t.ValueDate) @@ -159,4 +159,4 @@ namespace Schaad.Accounting.Services return fileBytes; } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Services/Interfaces/IChartService.cs b/Schaad.Accounting.Services/Interfaces/IChartService.cs index fd59f9c..c722964 100644 --- a/Schaad.Accounting.Services/Interfaces/IChartService.cs +++ b/Schaad.Accounting.Services/Interfaces/IChartService.cs @@ -1,10 +1,11 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Datasets.Charts; namespace Schaad.Accounting.Interfaces { public interface IChartService { - IReadOnlyList GetExpensesPerMonth(); + Task> GetExpensesPerMonthAsync(); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Services/Interfaces/IFileService.cs b/Schaad.Accounting.Services/Interfaces/IFileService.cs index bd3ecbc..a0990da 100644 --- a/Schaad.Accounting.Services/Interfaces/IFileService.cs +++ b/Schaad.Accounting.Services/Interfaces/IFileService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Datasets; using Schaad.Finance.Api; using Schaad.Finance.Api.Datasets; @@ -8,8 +9,8 @@ namespace Schaad.Accounting.Interfaces public interface IFileService { string Backup(); - IReadOnlyList ImportAccountStatementFile(string filePath); + Task> ImportAccountStatementFileAsync(string filePath); IReadOnlyList ImportCreditCardStatementFile(CreditCardProvider creditCardProvider, string filePath); - byte[] GetTransactionListCsv(string accountId); + Task GetTransactionListCsvAsync(string accountId); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Services/Interfaces/IViewService.cs b/Schaad.Accounting.Services/Interfaces/IViewService.cs index 4168725..9afc486 100644 --- a/Schaad.Accounting.Services/Interfaces/IViewService.cs +++ b/Schaad.Accounting.Services/Interfaces/IViewService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Threading.Tasks; using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets.Reports; using Schaad.Accounting.Models; @@ -8,24 +9,24 @@ namespace Schaad.Accounting.Interfaces { public interface IViewService { - AccountDataset GetAccountView(string id); + Task GetAccountViewAsync(string id); - List GetAccountViewList(); + Task> GetAccountViewListAsync(); - BalanceDataset GetBalanceView(); + Task GetBalanceViewAsync(); - BalanceSheetDataset GetBalanceSheetView(int year); + Task GetBalanceSheetViewAsync(int year); - List GetTransactionViewList(); + Task> GetTransactionViewListAsync(); - List GetTransactionViewList(string accountId); + Task> GetTransactionViewListAsync(string accountId); - List GetBookingRuleViewList(); + Task> GetBookingRuleViewListAsync(); - List GetOpenBankTransactionList(); + Task> GetOpenBankTransactionListAsync(); - List MatchOpenBankTransactions(); + Task> MatchOpenBankTransactionsAsync(); - List MatchCreditCardTransactions(string bankTransactionId, IReadOnlyList creditCardTransactions); + Task> MatchCreditCardTransactionsAsync(string bankTransactionId, IReadOnlyList creditCardTransactions); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Services/ViewService.cs b/Schaad.Accounting.Services/ViewService.cs index 7b41005..b59682f 100644 --- a/Schaad.Accounting.Services/ViewService.cs +++ b/Schaad.Accounting.Services/ViewService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets.Reports; @@ -38,23 +39,17 @@ namespace Schaad.Accounting.Services this.logger = logger; } - /// - /// Load accounts - /// - public AccountDataset GetAccountView(string id) + public async Task GetAccountViewAsync(string id) { - var accounts = GetAccountViewList(); + var accounts = await GetAccountViewListAsync(); return accounts.Single(t => t.Id == id); } - /// - /// Load accounts and calculates their balances - /// - public List GetAccountViewList() + public async Task> GetAccountViewListAsync() { - var accounts = accountRepository.GetAccountList(); - var transactionList = GetTransactionViewList(); - var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name); + var accounts = await accountRepository.GetAccountListAsync(); + var transactionList = await GetTransactionViewListAsync(); + var subClassNameByNumber = (await subclassRepository.GetSubClassListAsync()).ToDictionary(s => s.Number, s => s.Name); // Group transactions by account so per-account balance is O(k) instead of O(M). var creditsByAccount = transactionList.GroupBy(t => t.TargetAccountId).ToDictionary(g => g.Key, g => g.ToList()); @@ -93,9 +88,9 @@ namespace Schaad.Accounting.Services return balance; } - public BalanceDataset GetBalanceView() + public async Task GetBalanceViewAsync() { - var accountList = GetAccountViewList(); + var accountList = await GetAccountViewListAsync(); return new BalanceDataset( ActivaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), PassivaAccountList: accountList.Where(m => m.Class == ClassIds.Passiva).ToList(), @@ -103,32 +98,30 @@ namespace Schaad.Accounting.Services TotalPassivaCHF: Math.Abs(accountList.Where(m => m.Class == ClassIds.Passiva).Sum(m => m.BalanceCHF))); } - public BalanceSheetDataset GetBalanceSheetView(int year) + public async Task GetBalanceSheetViewAsync(int year) { - var accountList = GetAccountViewList(); + var accountList = await GetAccountViewListAsync(); var profit = Math.Abs(accountList.Where(m => m.Class == ClassIds.Income).Sum(m => m.BalanceCHF)); profit += Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => fxConverter.ConvertToChf(m.StartBalance, m.Currency))); var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF)); - var balanceView = new BalanceSheetDataset( + return new BalanceSheetDataset( ActivaAccountList: accountList.Where(m => m.Class == ClassIds.Activa).ToList(), IncomeAccountList: accountList.Where(m => m.Class == ClassIds.Income).ToList(), ExpensesAccountList: accountList.Where(m => m.Class == ClassIds.Expenses).ToList(), ProfitCHF: profit, LossCHF: loss, Year: year); - - return balanceView; } /// /// Get transaction list with the origin and target account for each transaction /// - public List GetTransactionViewList() + public async Task> GetTransactionViewListAsync() { - var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); - var transactionList = transactionRepository.GetTransactionList(); + var accountsById = (await accountRepository.GetAccountListAsync()).ToDictionary(a => a.Id); + var transactionList = await transactionRepository.GetTransactionListAsync(); return transactionList.Select(t => new TransactionDataset( @@ -139,12 +132,13 @@ namespace Schaad.Accounting.Services } /// - /// Get transaction list with the origin and target account for each transaction + /// Get transaction list filtered to a single account, with values sign-flipped + /// when the requested account is the origin of an activa booking. /// - public List GetTransactionViewList(string accountId) + public async Task> GetTransactionViewListAsync(string accountId) { - var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); - var transactionList = transactionRepository.GetTransactionList() + var accountsById = (await accountRepository.GetAccountListAsync()).ToDictionary(a => a.Id); + var transactionList = (await transactionRepository.GetTransactionListAsync()) .Where(t => t.OriginAccountId == accountId || t.TargetAccountId == accountId); return transactionList.Select(t => @@ -170,10 +164,10 @@ namespace Schaad.Accounting.Services /// /// Get booking rules with their account /// - public List GetBookingRuleViewList() + public async Task> GetBookingRuleViewListAsync() { - var accountsById = accountRepository.GetAccountList().ToDictionary(a => a.Id); - var bookingRules = bookingRuleRepository.GetBookingRuleList(); + var accountsById = (await accountRepository.GetAccountListAsync()).ToDictionary(a => a.Id); + var bookingRules = await bookingRuleRepository.GetBookingRuleListAsync(); return bookingRules.Select(t => new BookingRuleDataset(t, accountsById[t.AccountId].Name)) @@ -183,28 +177,26 @@ namespace Schaad.Accounting.Services /// /// Load open bank transactions /// - public List GetOpenBankTransactionList() + public async Task> GetOpenBankTransactionListAsync() { - var transactions = transactionRepository - .GetTransactionList() + var transactions = (await transactionRepository.GetTransactionListAsync()) .Where(t => string.IsNullOrEmpty(t.BankTransactionId) == false) .Select(t => t.BankTransactionId); - var bankTransactions = bankTransactionRepository - .GetBankTransactionList() + var bankTransactions = (await bankTransactionRepository.GetBankTransactionListAsync()) .Where(b => b.Ignore == false && transactions.Contains(b.Id) == false) .ToList(); return bankTransactions; } - public List MatchOpenBankTransactions() + public async Task> MatchOpenBankTransactionsAsync() { var newTransactionList = new List(); - var transactions = transactionRepository.GetTransactionList(); - var accounts = accountRepository.GetAccountList(); - var bookingRules = bookingRuleRepository.GetBookingRuleList(); - var bankTransactions = GetOpenBankTransactionList().OrderBy(t => t.ValueDate).ToList(); + var transactions = await transactionRepository.GetTransactionListAsync(); + var accounts = await accountRepository.GetAccountListAsync(); + var bookingRules = await bookingRuleRepository.GetBookingRuleListAsync(); + var bankTransactions = (await GetOpenBankTransactionListAsync()).OrderBy(t => t.ValueDate).ToList(); foreach (var bankTransaction in bankTransactions) { var trx = new Transaction(bankTransaction, accounts); @@ -216,14 +208,15 @@ namespace Schaad.Accounting.Services var matched = newTransactionList.Count(t => string.IsNullOrEmpty(t.Text) == false); logger.LogInformation("Matched {Matched} of {Total} open bank transactions", matched, bankTransactions.Count); + return newTransactionList; } - public List MatchCreditCardTransactions(string bankTransactionId, IReadOnlyList creditCardTransactions) + public async Task> MatchCreditCardTransactionsAsync(string bankTransactionId, IReadOnlyList creditCardTransactions) { - var accounts = accountRepository.GetAccountList(); - var bankTrx = bankTransactionRepository.GetBankTransaction(bankTransactionId); - var bookingRules = bookingRuleRepository.GetBookingRuleList(); + var accounts = await accountRepository.GetAccountListAsync(); + var bankTrx = await bankTransactionRepository.GetBankTransactionAsync(bankTransactionId); + var bookingRules = await bookingRuleRepository.GetBookingRuleListAsync(); var trxList = new List(); foreach (var creditCardTransaction in creditCardTransactions) @@ -351,4 +344,4 @@ namespace Schaad.Accounting.Services } } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.Tests/AccountRepositoryTestShould.cs b/Schaad.Accounting.Tests/AccountRepositoryTestShould.cs index 59a1a2c..4511fb7 100644 --- a/Schaad.Accounting.Tests/AccountRepositoryTestShould.cs +++ b/Schaad.Accounting.Tests/AccountRepositoryTestShould.cs @@ -34,79 +34,79 @@ public class AccountRepositoryTestShould : IDisposable } [Fact] - public void AssignNewIdWhenSavingAccountWithoutId() + public async Task AssignNewIdWhenSavingAccountWithoutId() { var account = new Account { Number = 1000, Name = "Cash", Currency = "CHF" }; - sut.SaveAccount(account); + await sut.SaveAccountAsync(account); - var stored = sut.GetAccountList().Single(); + var stored = (await sut.GetAccountListAsync()).Single(); stored.Id.ShouldNotBeNullOrEmpty(); stored.Name.ShouldBe("Cash"); } [Fact] - public void UpdateInPlaceWhenSavingAccountWithExistingId() + public async Task UpdateInPlaceWhenSavingAccountWithExistingId() { - sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); - var id = sut.GetAccountList().Single().Id; + await sut.SaveAccountAsync(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); + var id = (await sut.GetAccountListAsync()).Single().Id; - sut.SaveAccount(new Account { Id = id, Number = 1000, Name = "Cash renamed", Currency = "CHF" }); + await sut.SaveAccountAsync(new Account { Id = id, Number = 1000, Name = "Cash renamed", Currency = "CHF" }); - var accounts = sut.GetAccountList(); + var accounts = await sut.GetAccountListAsync(); accounts.Count.ShouldBe(1); accounts[0].Name.ShouldBe("Cash renamed"); } [Fact] - public void DefaultCurrencyToChfWhenSavingAccountWithoutCurrency() + public async Task DefaultCurrencyToChfWhenSavingAccountWithoutCurrency() { - sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = null! }); + await sut.SaveAccountAsync(new Account { Number = 1000, Name = "Cash", Currency = null! }); - sut.GetAccountList().Single().Currency.ShouldBe("CHF"); + (await sut.GetAccountListAsync()).Single().Currency.ShouldBe("CHF"); } [Fact] - public void RemoveAccountWhenDeletingById() + public async Task RemoveAccountWhenDeletingById() { - sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); - var id = sut.GetAccountList().Single().Id; + await sut.SaveAccountAsync(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); + var id = (await sut.GetAccountListAsync()).Single().Id; - sut.DeleteAccount(id); + await sut.DeleteAccountAsync(id); - sut.GetAccountList().ShouldBeEmpty(); + (await sut.GetAccountListAsync()).ShouldBeEmpty(); } [Fact] - public void ReturnAccountWhenLookingUpByBankAccountNumber() + public async Task ReturnAccountWhenLookingUpByBankAccountNumber() { - sut.SaveAccount(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" }); - sut.SaveAccount(new Account { Number = 1020, Name = "Savings", Currency = "CHF", BankAccountNumber = "CH02" }); + await sut.SaveAccountAsync(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" }); + await sut.SaveAccountAsync(new Account { Number = 1020, Name = "Savings", Currency = "CHF", BankAccountNumber = "CH02" }); - sut.GetAccountForBankAccountNumber("CH01").Name.ShouldBe("Checking"); - sut.GetAccountForBankAccountNumber("CH02").Name.ShouldBe("Savings"); - sut.GetAccountForBankAccountNumber("CH99").ShouldBeNull(); + (await sut.GetAccountForBankAccountNumberAsync("CH01")).Name.ShouldBe("Checking"); + (await sut.GetAccountForBankAccountNumberAsync("CH02")).Name.ShouldBe("Savings"); + (await sut.GetAccountForBankAccountNumberAsync("CH99")).ShouldBeNull(); } [Fact] - public void UpdateLastBankBalanceWhenSavingBankBalance() + public async Task UpdateLastBankBalanceWhenSavingBankBalance() { - sut.SaveAccount(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" }); + await sut.SaveAccountAsync(new Account { Number = 1010, Name = "Checking", Currency = "CHF", BankAccountNumber = "CH01" }); - sut.SaveBankAccountBalance("CH01", 1234.56m); + await sut.SaveBankAccountBalanceAsync("CH01", 1234.56m); - sut.GetAccountForBankAccountNumber("CH01").LastBankBalance.ShouldBe(1234.56m); + (await sut.GetAccountForBankAccountNumberAsync("CH01")).LastBankBalance.ShouldBe(1234.56m); } [Fact] - public void SurviveConstructorReRunWhenAccountsFileAlreadyExists() + public async Task SurviveConstructorReRunWhenAccountsFileAlreadyExists() { - sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); + await sut.SaveAccountAsync(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); // Re-construct with the same directory: existing file, no year rollover, no data loss. var fresh = new AccountRepository(settingsService, new RepositoryCache(), NullLogger.Instance); - var accounts = fresh.GetAccountList(); + var accounts = await fresh.GetAccountListAsync(); accounts.Count.ShouldBe(1); accounts[0].Name.ShouldBe("Cash"); } diff --git a/Schaad.Accounting.Tests/ChartServiceTestShould.cs b/Schaad.Accounting.Tests/ChartServiceTestShould.cs index 93943c3..1f1023f 100644 --- a/Schaad.Accounting.Tests/ChartServiceTestShould.cs +++ b/Schaad.Accounting.Tests/ChartServiceTestShould.cs @@ -20,18 +20,18 @@ public class ChartServiceTestShould private ChartService BuildService() => new(settingsService, viewService); [Fact] - public void HonourSelectedYearWhenBuildingMonthlySeries() + public async Task HonourSelectedYearWhenBuildingMonthlySeries() { settingsService.GetYear().Returns(2023); var groceries = MakeAccount("groc", 4200); var checking = MakeAccount("chk", 1010); - viewService.GetAccountViewList().Returns(new List { AsDataset(groceries), AsDataset(checking) }); - viewService.GetTransactionViewList().Returns(new List + viewService.GetAccountViewListAsync().Returns(new List { AsDataset(groceries), AsDataset(checking) }); + viewService.GetTransactionViewListAsync().Returns(new List { MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2023, 4, 5)) }); - var series = BuildService().GetExpensesPerMonth(); + var series = await BuildService().GetExpensesPerMonthAsync(); series.Count.ShouldBe(1); series[0].X.ShouldAllBe(x => ((DateOnly)x).Year == 2023); @@ -40,22 +40,22 @@ public class ChartServiceTestShould } [Fact] - public void GroupExpensesByAccountWhenAllExpensesShareOneSubClass() + public async Task GroupExpensesByAccountWhenAllExpensesShareOneSubClass() { - var mannenbach = MakeAccount("m1", 4000); // SubClass 40 - var mannenbach2 = MakeAccount("m2", 4001); // SubClass 40 (same) + var mannenbach = MakeAccount("m1", 4000); + var mannenbach2 = MakeAccount("m2", 4001); var checking = MakeAccount("chk", 1010); - viewService.GetAccountViewList().Returns(new List + viewService.GetAccountViewListAsync().Returns(new List { AsDataset(mannenbach), AsDataset(mannenbach2), AsDataset(checking) }); - viewService.GetTransactionViewList().Returns(new List + viewService.GetTransactionViewListAsync().Returns(new List { MakeTransaction(checking, mannenbach, value: 10m, valueDate: new DateTime(2026, 1, 1)), MakeTransaction(checking, mannenbach2, value: 20m, valueDate: new DateTime(2026, 1, 1)) }); - var series = BuildService().GetExpensesPerMonth(); + var series = await BuildService().GetExpensesPerMonthAsync(); series.Count.ShouldBe(2); series.ShouldContain(s => s.Id == "m1"); @@ -63,44 +63,44 @@ public class ChartServiceTestShould } [Fact] - public void GroupExpensesBySubClassWhenMultipleSubClassesAreUsed() + public async Task GroupExpensesBySubClassWhenMultipleSubClassesAreUsed() { - var groceries = MakeAccount("groc", 4200); // SubClass 42 - var utilities = MakeAccount("util", 4300); // SubClass 43 + var groceries = MakeAccount("groc", 4200); + var utilities = MakeAccount("util", 4300); var checking = MakeAccount("chk", 1010); - viewService.GetAccountViewList().Returns(new List + viewService.GetAccountViewListAsync().Returns(new List { AsDataset(groceries), AsDataset(utilities), AsDataset(checking) }); - viewService.GetTransactionViewList().Returns(new List + viewService.GetTransactionViewListAsync().Returns(new List { MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 1)), MakeTransaction(checking, utilities, value: 30m, valueDate: new DateTime(2026, 2, 1)) }); - var series = BuildService().GetExpensesPerMonth(); + var series = await BuildService().GetExpensesPerMonthAsync(); series.Count.ShouldBe(2); - series.ShouldContain(s => s.Id == "42"); // sub-class number as id + series.ShouldContain(s => s.Id == "42"); series.ShouldContain(s => s.Id == "43"); } [Fact] - public void SkipAccountsWithoutTransactionsWhenBuildingSeries() + public async Task SkipAccountsWithoutTransactionsWhenBuildingSeries() { - var groceries = MakeAccount("groc", 4000); // has activity - var utilities = MakeAccount("util", 4001); // same subclass, no activity + var groceries = MakeAccount("groc", 4000); + var utilities = MakeAccount("util", 4001); var checking = MakeAccount("chk", 1010); - viewService.GetAccountViewList().Returns(new List + viewService.GetAccountViewListAsync().Returns(new List { AsDataset(groceries), AsDataset(utilities), AsDataset(checking) }); - viewService.GetTransactionViewList().Returns(new List + viewService.GetTransactionViewListAsync().Returns(new List { MakeTransaction(checking, groceries, value: 50m, valueDate: new DateTime(2026, 1, 1)) }); - var series = BuildService().GetExpensesPerMonth(); + var series = await BuildService().GetExpensesPerMonthAsync(); series.Count.ShouldBe(1); series[0].Id.ShouldBe("groc"); diff --git a/Schaad.Accounting.Tests/FileServiceTestShould.cs b/Schaad.Accounting.Tests/FileServiceTestShould.cs index 3d3f966..44480fe 100644 --- a/Schaad.Accounting.Tests/FileServiceTestShould.cs +++ b/Schaad.Accounting.Tests/FileServiceTestShould.cs @@ -20,8 +20,6 @@ public class FileServiceTestShould public FileServiceTestShould() { - // Registering the ISO-8859-1 code page provider so the CSV encoding call works - // in the test host (which does not include legacy encodings by default). Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); } @@ -30,13 +28,11 @@ public class FileServiceTestShould accountStatementService, creditCardStatementService, NullLogger.Instance); [Fact] - public void EmitHeaderRowAndRunningBalanceWhenExportingTransactionsCsv() + public async Task EmitHeaderRowAndRunningBalanceWhenExportingTransactionsCsv() { var checking = new Account { Id = "chk", Number = 1010, Name = "Checking", Currency = "CHF", StartBalance = 1000m }; - var groceries = new Account { Id = "groc", Number = 4200, Name = "Groceries", Currency = "CHF" }; - var salary = new Account { Id = "sal", Number = 3000, Name = "Salary", Currency = "CHF" }; - accountRepo.GetAccount("chk").Returns(checking); - transactionRepo.GetTransactionList().Returns(new List + accountRepo.GetAccountAsync("chk").Returns(checking); + transactionRepo.GetTransactionListAsync().Returns(new List { new() { @@ -50,26 +46,23 @@ public class FileServiceTestShould } }); - var bytes = BuildService().GetTransactionListCsv("chk"); + var bytes = await BuildService().GetTransactionListCsvAsync("chk"); var csv = Encoding.GetEncoding("ISO-8859-1").GetString(bytes); var lines = csv.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); lines[0].ShouldBe("Buchungsdatum;Valuta;Buchungstext;Belastung;Gutschrift;Saldo CHF"); lines[1].ShouldBe(";;Startsaldo;;;1000"); - // Debit line (outgoing) — value in Belastung column, balance drops lines[2].ShouldBe("05.01.2026;05.01.2026;Coop;50.00;;950.00"); - // Credit line (incoming) — value in Gutschrift column, balance rises lines[3].ShouldBe("25.01.2026;25.01.2026;Salary;;5'000.00;5'950.00"); } [Fact] - public void OrderTransactionsByBookingDateThenValueDateThenValueWhenExporting() + public async Task OrderTransactionsByBookingDateThenValueDateThenValueWhenExporting() { var checking = new Account { Id = "chk", Number = 1010, Name = "Checking", Currency = "CHF", StartBalance = 0m }; - accountRepo.GetAccount("chk").Returns(checking); - transactionRepo.GetTransactionList().Returns(new List + accountRepo.GetAccountAsync("chk").Returns(checking); + transactionRepo.GetTransactionListAsync().Returns(new List { - // Deliberately unordered new() { OriginAccountId = "chk", TargetAccountId = "x", Value = 10m, Text = "later", BookingDate = new DateTime(2026, 2, 1), ValueDate = new DateTime(2026, 2, 1) }, new() { OriginAccountId = "chk", TargetAccountId = "x", Value = 5m, Text = "earlier small", @@ -78,30 +71,28 @@ public class FileServiceTestShould BookingDate = new DateTime(2026, 1, 15), ValueDate = new DateTime(2026, 1, 15) } }); - var csv = Encoding.GetEncoding("ISO-8859-1").GetString(BuildService().GetTransactionListCsv("chk")); + var csv = Encoding.GetEncoding("ISO-8859-1").GetString(await BuildService().GetTransactionListCsvAsync("chk")); var lines = csv.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); - // Skip header + start balance rows lines[2].ShouldContain("earlier small"); lines[3].ShouldContain("earlier large"); lines[4].ShouldContain("later"); } [Fact] - public void NotMutateTransactionValueWhenExportingCsv() + public async Task NotMutateTransactionValueWhenExportingCsv() { var checking = new Account { Id = "chk", Number = 1010, Name = "Checking", Currency = "CHF" }; - accountRepo.GetAccount("chk").Returns(checking); + accountRepo.GetAccountAsync("chk").Returns(checking); var outgoing = new Transaction { OriginAccountId = "chk", TargetAccountId = "x", Value = 42m, Text = "test", BookingDate = new DateTime(2026, 1, 1), ValueDate = new DateTime(2026, 1, 1) }; - transactionRepo.GetTransactionList().Returns(new List { outgoing }); + transactionRepo.GetTransactionListAsync().Returns(new List { outgoing }); - BuildService().GetTransactionListCsv("chk"); + await BuildService().GetTransactionListCsvAsync("chk"); - // Regression: earlier code did `trx.Value *= -1` mid-loop. outgoing.Value.ShouldBe(42m); } } diff --git a/Schaad.Accounting.Tests/RepositoryCacheTestShould.cs b/Schaad.Accounting.Tests/RepositoryCacheTestShould.cs index 776044d..de4fd96 100644 --- a/Schaad.Accounting.Tests/RepositoryCacheTestShould.cs +++ b/Schaad.Accounting.Tests/RepositoryCacheTestShould.cs @@ -6,13 +6,13 @@ namespace Schaad.Accounting.Tests; public class RepositoryCacheTestShould { [Fact] - public void CallLoaderOnceWhenKeyIsRequestedRepeatedly() + public async Task CallLoaderOnceWhenKeyIsRequestedRepeatedly() { var cache = new RepositoryCache(); var loaderCalls = 0; - var first = cache.GetOrLoad("key", () => { loaderCalls++; return new List { 1, 2 }; }); - var second = cache.GetOrLoad("key", () => { loaderCalls++; return new List { 99 }; }); + var first = await cache.GetOrLoadAsync("key", () => { loaderCalls++; return Task.FromResult(new List { 1, 2 }); }); + var second = await cache.GetOrLoadAsync("key", () => { loaderCalls++; return Task.FromResult(new List { 99 }); }); loaderCalls.ShouldBe(1); second.ShouldBeSameAs(first); @@ -20,12 +20,12 @@ public class RepositoryCacheTestShould } [Fact] - public void KeepEntriesSeparateWhenKeysDiffer() + public async Task KeepEntriesSeparateWhenKeysDiffer() { var cache = new RepositoryCache(); - var a = cache.GetOrLoad("a", () => new List { 1 }); - var b = cache.GetOrLoad("b", () => new List { 2 }); + var a = await cache.GetOrLoadAsync("a", () => Task.FromResult(new List { 1 })); + var b = await cache.GetOrLoadAsync("b", () => Task.FromResult(new List { 2 })); b.ShouldNotBeSameAs(a); a.ShouldBe(new[] { 1 }); @@ -33,25 +33,25 @@ public class RepositoryCacheTestShould } [Fact] - public void ReloadFromLoaderWhenKeyIsInvalidated() + public async Task ReloadFromLoaderWhenKeyIsInvalidated() { var cache = new RepositoryCache(); var loaderCalls = 0; - cache.GetOrLoad("k", () => { loaderCalls++; return new List { 1 }; }); + await cache.GetOrLoadAsync("k", () => { loaderCalls++; return Task.FromResult(new List { 1 }); }); cache.Invalidate("k"); - cache.GetOrLoad("k", () => { loaderCalls++; return new List { 2 }; }); + await cache.GetOrLoadAsync("k", () => { loaderCalls++; return Task.FromResult(new List { 2 }); }); loaderCalls.ShouldBe(2); } [Fact] - public void MatchKeysCaseInsensitivelyWhenLookingUp() + public async Task MatchKeysCaseInsensitivelyWhenLookingUp() { var cache = new RepositoryCache(); - cache.GetOrLoad("Path/To/File.xml", () => new List { 1 }); - var upper = cache.GetOrLoad("PATH/TO/FILE.XML", () => new List { 99 }); + await cache.GetOrLoadAsync("Path/To/File.xml", () => Task.FromResult(new List { 1 })); + var upper = await cache.GetOrLoadAsync("PATH/TO/FILE.XML", () => Task.FromResult(new List { 99 })); upper.ShouldBe(new[] { 1 }); } diff --git a/Schaad.Accounting.Tests/TransactionRepositoryTestShould.cs b/Schaad.Accounting.Tests/TransactionRepositoryTestShould.cs index 1e6960e..67c8746 100644 --- a/Schaad.Accounting.Tests/TransactionRepositoryTestShould.cs +++ b/Schaad.Accounting.Tests/TransactionRepositoryTestShould.cs @@ -38,12 +38,12 @@ public class TransactionRepositoryTestShould : IDisposable } [Fact] - public void PreserveValueWhenRoundTrippingNonFxTransaction() + public async Task PreserveValueWhenRoundTrippingNonFxTransaction() { - accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); - accountRepo.GetAccount("groc").Returns(new Account { Id = "groc", Currency = "CHF" }); + accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); + accountRepo.GetAccountAsync("groc").Returns(new Account { Id = "groc", Currency = "CHF" }); - sut.SaveTransaction(new Transaction + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "groc", @@ -53,22 +53,21 @@ public class TransactionRepositoryTestShould : IDisposable BookingDate = new DateTime(2026, 5, 10) }); - var stored = sut.GetTransactionList().Single(); - var loaded = sut.GetTransaction(stored.Id); + var stored = (await sut.GetTransactionListAsync()).Single(); + var loaded = await sut.GetTransactionAsync(stored.Id); loaded.ShouldNotBeNull(); loaded.Value.ShouldBe(42m); - loaded.FxRate.ShouldBeNull(); // Non-FX transactions have FxRate cleared + loaded.FxRate.ShouldBeNull(); } [Fact] - public void StoreInChfAndReturnInAccountCurrencyWhenTransactionIsFx() + public async Task StoreInChfAndReturnInAccountCurrencyWhenTransactionIsFx() { - // User enters 100 USD at rate 0.9 CHF/USD -> stored as 90 CHF; read back as 100 USD. - accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); - accountRepo.GetAccount("usd").Returns(new Account { Id = "usd", Currency = "USD" }); + accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); + accountRepo.GetAccountAsync("usd").Returns(new Account { Id = "usd", Currency = "USD" }); - sut.SaveTransaction(new Transaction + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "usd", @@ -79,28 +78,28 @@ public class TransactionRepositoryTestShould : IDisposable BookingDate = new DateTime(2026, 5, 10) }); - var stored = sut.GetTransactionList().Single(); - stored.Value.ShouldBe(90m); // stored in CHF + var stored = (await sut.GetTransactionListAsync()).Single(); + stored.Value.ShouldBe(90m); - var loaded = sut.GetTransaction(stored.Id); + var loaded = await sut.GetTransactionAsync(stored.Id); loaded.ShouldNotBeNull(); - loaded.Value.ShouldBe(100m); // display currency + loaded.Value.ShouldBe(100m); loaded.FxRate.ShouldBe(0.9m); } [Fact] - public void ReturnNullWhenGettingUnknownTransactionId() + public async Task ReturnNullWhenGettingUnknownTransactionId() { - sut.GetTransaction("no-such-id").ShouldBeNull(); + (await sut.GetTransactionAsync("no-such-id")).ShouldBeNull(); } [Fact] - public void NotMutateStoredValueWhenGettingFxTransactionTwice() + public async Task NotMutateStoredValueWhenGettingFxTransactionTwice() { - accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); - accountRepo.GetAccount("usd").Returns(new Account { Id = "usd", Currency = "USD" }); + accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); + accountRepo.GetAccountAsync("usd").Returns(new Account { Id = "usd", Currency = "USD" }); - sut.SaveTransaction(new Transaction + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "usd", @@ -111,12 +110,11 @@ public class TransactionRepositoryTestShould : IDisposable BookingDate = new DateTime(2026, 5, 10) }); - var stored = sut.GetTransactionList().Single(); + var stored = (await sut.GetTransactionListAsync()).Single(); var id = stored.Id; - // Get twice; if Get mutated the stored entity, the second call would divide by FxRate again. - var first = sut.GetTransaction(id); - var second = sut.GetTransaction(id); + var first = await sut.GetTransactionAsync(id); + var second = await sut.GetTransactionAsync(id); first.ShouldNotBeNull(); first.Value.ShouldBe(100m); @@ -125,24 +123,24 @@ public class TransactionRepositoryTestShould : IDisposable } [Fact] - public void RemoveTransactionWhenDeletingById() + public async Task RemoveTransactionWhenDeletingById() { - accountRepo.GetAccount(Arg.Any()).Returns(new Account { Currency = "CHF" }); + accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); - sut.SaveTransaction(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 1m, Text = "x", ValueDate = DateTime.Today }); - var id = sut.GetTransactionList().Single().Id; + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 1m, Text = "x", ValueDate = DateTime.Today }); + var id = (await sut.GetTransactionListAsync()).Single().Id; - sut.DeleteTransaction(id); + await sut.DeleteTransactionAsync(id); - sut.GetTransactionList().ShouldBeEmpty(); + (await sut.GetTransactionListAsync()).ShouldBeEmpty(); } [Fact] - public void PreserveRelatedPartyWhenRoundTrippingTransaction() + public async Task PreserveRelatedPartyWhenRoundTrippingTransaction() { - accountRepo.GetAccount(Arg.Any()).Returns(new Account { Currency = "CHF" }); + accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); - sut.SaveTransaction(new Transaction + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "a", TargetAccountId = "b", @@ -153,26 +151,23 @@ public class TransactionRepositoryTestShould : IDisposable BookingDate = new DateTime(2026, 4, 1) }); - // Regression: earlier Transaction.Copy dropped RelatedParty on every save, - // so the round-trip lost it. - sut.GetTransactionList().Single().RelatedParty.ShouldBe("ACME Property AG"); + (await sut.GetTransactionListAsync()).Single().RelatedParty.ShouldBe("ACME Property AG"); } [Fact] - public void DefaultBookingDateToValueDateWhenBookingDateIsUnset() + public async Task DefaultBookingDateToValueDateWhenBookingDateIsUnset() { - accountRepo.GetAccount(Arg.Any()).Returns(new Account { Currency = "CHF" }); + accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); - sut.SaveTransaction(new Transaction + await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 10m, Text = "x", ValueDate = new DateTime(2026, 4, 15) - // BookingDate left at default (DateTime.MinValue) }); - sut.GetTransactionList().Single().BookingDate.ShouldBe(new DateTime(2026, 4, 15)); + (await sut.GetTransactionListAsync()).Single().BookingDate.ShouldBe(new DateTime(2026, 4, 15)); } } diff --git a/Schaad.Accounting.Tests/ViewServiceTestShould.cs b/Schaad.Accounting.Tests/ViewServiceTestShould.cs index 46b5b79..b92dfda 100644 --- a/Schaad.Accounting.Tests/ViewServiceTestShould.cs +++ b/Schaad.Accounting.Tests/ViewServiceTestShould.cs @@ -18,12 +18,10 @@ public class ViewServiceTestShould public ViewServiceTestShould() { - // Cover every subclass number a test might reference (Account.SubClass = Number / 100). - subclassRepo.GetSubClassList().Returns( + subclassRepo.GetSubClassListAsync().Returns( Enumerable.Range(10, 50).Select(n => new SubClass { Number = n, Name = "sub-" + n }).ToList()); subclassRepo.GetClass(Arg.Any()).Returns(""); - // Passthrough FX by default (CHF-only). Individual tests can override. fxConverter.ConvertToChf(Arg.Any(), Arg.Any()) .Returns(ci => ci.ArgAt(0)); } @@ -34,16 +32,16 @@ public class ViewServiceTestShould // --- Balance math --------------------------------------------------------- [Fact] - public void SumActivaAndPassivaTotalsSeparatelyWhenGettingBalanceView() + public async Task SumActivaAndPassivaTotalsSeparatelyWhenGettingBalanceView() { var cash = Account("cash", 1000, "CHF", startBalance: 100m); var receivables = Account("recv", 1100, "CHF", startBalance: 50m); var payables = Account("pay", 2000, "CHF", startBalance: -200m); - accountRepo.GetAccountList().Returns(new List { cash, receivables, payables }); - transactionRepo.GetTransactionList().Returns(new List()); + accountRepo.GetAccountListAsync().Returns(new List { cash, receivables, payables }); + transactionRepo.GetTransactionListAsync().Returns(new List()); - var view = BuildService().GetBalanceView(); + var view = await BuildService().GetBalanceViewAsync(); view.ActivaAccountList.Count.ShouldBe(2); view.PassivaAccountList.Count.ShouldBe(1); @@ -52,109 +50,105 @@ public class ViewServiceTestShould } [Fact] - public void ApplyTransactionsToStartBalanceWhenComputingAccountBalance() + public async Task ApplyTransactionsToStartBalanceWhenComputingAccountBalance() { var cash = Account("cash", 1000, "CHF", startBalance: 1000m); var groceries = Account("groc", 4000, "CHF"); - accountRepo.GetAccountList().Returns(new List { cash, groceries }); + accountRepo.GetAccountListAsync().Returns(new List { cash, groceries }); - // Debit cash 50, credit groceries 50 (spending) - transactionRepo.GetTransactionList().Returns(new List + transactionRepo.GetTransactionListAsync().Returns(new List { new() { Id = "t1", OriginAccountId = "cash", TargetAccountId = "groc", Value = 50m, ValueDate = new DateTime(2026, 1, 1) }, new() { Id = "t2", OriginAccountId = "cash", TargetAccountId = "groc", Value = 30m, ValueDate = new DateTime(2026, 1, 2) }, }); - var accounts = BuildService().GetAccountViewList(); + var accounts = await BuildService().GetAccountViewListAsync(); - accounts.Single(a => a.Id == "cash").Balance.ShouldBe(920m); // 1000 - 80 + accounts.Single(a => a.Id == "cash").Balance.ShouldBe(920m); accounts.Single(a => a.Id == "groc").Balance.ShouldBe(80m); } [Fact] - public void ConvertBalanceToChfWhenAccountUsesForeignCurrency() + public async Task ConvertBalanceToChfWhenAccountUsesForeignCurrency() { var usd = Account("usd", 1050, "USD", startBalance: 100m); - accountRepo.GetAccountList().Returns(new List { usd }); - transactionRepo.GetTransactionList().Returns(new List()); + accountRepo.GetAccountListAsync().Returns(new List { usd }); + transactionRepo.GetTransactionListAsync().Returns(new List()); - // 1 USD = 0.90 CHF fxConverter.ConvertToChf(Arg.Any(), "USD") .Returns(ci => ci.ArgAt(0) * 0.9m); - var account = BuildService().GetAccountViewList().Single(); + var account = (await BuildService().GetAccountViewListAsync()).Single(); - account.Balance.ShouldBe(100m); // account currency - account.BalanceCHF.ShouldBe(90m); // FX-converted + account.Balance.ShouldBe(100m); + account.BalanceCHF.ShouldBe(90m); account.StartBalanceCHF.ShouldBe(90m); } // --- Bank-transaction auto-matching -------------------------------------- [Fact] - public void MatchBankTransactionByBookingRuleWhenLookupTextIsContained() + public async Task MatchBankTransactionByBookingRuleWhenLookupTextIsContained() { var checking = Account("chk", 1010, "CHF", bankAccountNumber: "CH01"); var utilities = Account("util", 4200, "CHF"); - accountRepo.GetAccountList().Returns(new List { checking, utilities }); - bookingRuleRepo.GetBookingRuleList().Returns(new List + accountRepo.GetAccountListAsync().Returns(new List { checking, utilities }); + bookingRuleRepo.GetBookingRuleListAsync().Returns(new List { new() { Id = "r1", LookupText = "electric", AccountId = "util", BookingText = "Strom" } }); - bankTransactionRepo.GetBankTransactionList().Returns(new List + bankTransactionRepo.GetBankTransactionListAsync().Returns(new List { new() { Id = "b1", Text = "Monthly Electric Bill", Value = -85m, BankAccountNumber = "CH01", ValueDate = new DateTime(2026, 3, 5), BookingDate = new DateTime(2026, 3, 5) } }); - transactionRepo.GetTransactionList().Returns(new List()); + transactionRepo.GetTransactionListAsync().Returns(new List()); - var matched = BuildService().MatchOpenBankTransactions().Single(); + var matched = (await BuildService().MatchOpenBankTransactionsAsync()).Single(); matched.Text.ShouldBe("Strom"); - // negative value => bank account is origin; rule fills the target matched.OriginAccountId.ShouldBe("chk"); matched.TargetAccountId.ShouldBe("util"); } [Fact] - public void PreferBookingRuleWithMatchingValueWhenMultipleRulesMatchText() + public async Task PreferBookingRuleWithMatchingValueWhenMultipleRulesMatchText() { var checking = Account("chk", 1010, "CHF", bankAccountNumber: "CH01"); var rent = Account("rent", 4100, "CHF"); var groceries = Account("groc", 4200, "CHF"); - accountRepo.GetAccountList().Returns(new List { checking, rent, groceries }); - bookingRuleRepo.GetBookingRuleList().Returns(new List + accountRepo.GetAccountListAsync().Returns(new List { checking, rent, groceries }); + bookingRuleRepo.GetBookingRuleListAsync().Returns(new List { new() { Id = "r1", LookupText = "coop", LookupValue = 0m, AccountId = "groc", BookingText = "Groceries" }, new() { Id = "r2", LookupText = "coop", LookupValue = 1200m, AccountId = "rent", BookingText = "Rent via Coop" } }); - bankTransactionRepo.GetBankTransactionList().Returns(new List + bankTransactionRepo.GetBankTransactionListAsync().Returns(new List { new() { Id = "b1", Text = "COOP payment", Value = -1200m, BankAccountNumber = "CH01", ValueDate = new DateTime(2026, 3, 1), BookingDate = new DateTime(2026, 3, 1) } }); - transactionRepo.GetTransactionList().Returns(new List()); + transactionRepo.GetTransactionListAsync().Returns(new List()); - var matched = BuildService().MatchOpenBankTransactions().Single(); + var matched = (await BuildService().MatchOpenBankTransactionsAsync()).Single(); matched.Text.ShouldBe("Rent via Coop"); matched.TargetAccountId.ShouldBe("rent"); } [Fact] - public void FallBackToSameAccountsLastMonthWhenNoBookingRuleMatches() + public async Task FallBackToSameAccountsLastMonthWhenNoBookingRuleMatches() { var checking = Account("chk", 1010, "CHF", bankAccountNumber: "CH01"); var salary = Account("sal", 3000, "CHF"); - accountRepo.GetAccountList().Returns(new List { checking, salary }); - bookingRuleRepo.GetBookingRuleList().Returns(new List()); - bankTransactionRepo.GetBankTransactionList().Returns(new List + accountRepo.GetAccountListAsync().Returns(new List { checking, salary }); + bookingRuleRepo.GetBookingRuleListAsync().Returns(new List()); + bankTransactionRepo.GetBankTransactionListAsync().Returns(new List { new() { Id = "b2", Text = "Salary Feb", Value = 5000m, BankAccountNumber = "CH01", ValueDate = new DateTime(2026, 2, 25), BookingDate = new DateTime(2026, 2, 25) } }); - // A prior-month booking with same accounts + value should be picked up - transactionRepo.GetTransactionList().Returns(new List + transactionRepo.GetTransactionListAsync().Returns(new List { new() { @@ -168,33 +162,31 @@ public class ViewServiceTestShould } }); - var matched = BuildService().MatchOpenBankTransactions().Single(); + var matched = (await BuildService().MatchOpenBankTransactionsAsync()).Single(); matched.Text.ShouldBe("Monatslohn"); } [Fact] - public void ExcludeIgnoredAndAlreadyBookedWhenGettingOpenBankTransactions() + public async Task ExcludeIgnoredAndAlreadyBookedWhenGettingOpenBankTransactions() { - bankTransactionRepo.GetBankTransactionList().Returns(new List + bankTransactionRepo.GetBankTransactionListAsync().Returns(new List { new() { Id = "open", Text = "still open", Ignore = false }, new() { Id = "ignored", Text = "ignore me", Ignore = true }, new() { Id = "booked", Text = "already booked", Ignore = false } }); - transactionRepo.GetTransactionList().Returns(new List + transactionRepo.GetTransactionListAsync().Returns(new List { new() { Id = "t1", BankTransactionId = "booked", OriginAccountId = "a", TargetAccountId = "b", Value = 10 } }); - var open = BuildService().GetOpenBankTransactionList(); + var open = await BuildService().GetOpenBankTransactionListAsync(); open.Count.ShouldBe(1); open[0].Id.ShouldBe("open"); } - // --- Helpers ------------------------------------------------------------- - private static Account Account(string id, int number, string currency, decimal startBalance = 0m, string? bankAccountNumber = null) => new() { diff --git a/Schaad.Accounting.UI/Components/Pages/Accounts.razor.cs b/Schaad.Accounting.UI/Components/Pages/Accounts.razor.cs index 7078a7d..5e225da 100644 --- a/Schaad.Accounting.UI/Components/Pages/Accounts.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Accounts.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -8,20 +8,20 @@ namespace Schaad.Accounting.UI.Components.Pages; public partial class Accounts : ComponentBase { - [Inject] + [Inject] private IAccountRepository accountRepository { get; set; } = null!; - + [Inject] private IDialogService dialogService { get; set; } = null!; - + private IQueryable? accountQueryable; - - protected override Task OnInitializedAsync() - { - accountQueryable = accountRepository.GetAccountList().AsQueryable(); - return base.OnInitializedAsync(); + + protected override async Task OnInitializedAsync() + { + accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); + await base.OnInitializedAsync(); } - + private async Task AddAsync() { var data = new Account(); @@ -36,13 +36,13 @@ public partial class Accounts : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - accountQueryable = accountRepository.GetAccountList().AsQueryable(); + accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); } } - + private async Task EditAsync(string id) { - var data = accountRepository.GetAccount(id); + var data = await accountRepository.GetAccountAsync(id); var dialog = await dialogService.ShowDialogAsync(data, new DialogParameters() { Height = "500px", @@ -54,19 +54,19 @@ public partial class Accounts : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - accountQueryable = accountRepository.GetAccountList().AsQueryable(); + accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); } } - + private async Task DeleteAsync(string id) { - var account = accountRepository.GetAccount(id); + var account = await accountRepository.GetAccountAsync(id); var dialog = await dialogService.ShowConfirmationAsync($"Konto '{account.Name}' wirklich löschen?", "Ja", "Nein", "Konto löschen"); var result = await dialog.Result; if (!result.Cancelled) { - accountRepository.DeleteAccount(id); - accountQueryable = accountRepository.GetAccountList().AsQueryable(); + await accountRepository.DeleteAccountAsync(id); + accountQueryable = (await accountRepository.GetAccountListAsync()).AsQueryable(); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.UI/Components/Pages/BankTransactions.razor.cs b/Schaad.Accounting.UI/Components/Pages/BankTransactions.razor.cs index f15ed04..b2ece4a 100644 --- a/Schaad.Accounting.UI/Components/Pages/BankTransactions.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/BankTransactions.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -9,10 +9,10 @@ public partial class BankTransactions : ComponentBase [Inject] private IBankTransactionRepository bankTransactionRepository { get; set; } = null!; private IQueryable? TransactionList; - - protected override Task OnInitializedAsync() - { - TransactionList = bankTransactionRepository.GetBankTransactionList().AsQueryable(); - return base.OnInitializedAsync(); + + protected override async Task OnInitializedAsync() + { + TransactionList = (await bankTransactionRepository.GetBankTransactionListAsync()).AsQueryable(); + await base.OnInitializedAsync(); } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.UI/Components/Pages/BookingRules.razor.cs b/Schaad.Accounting.UI/Components/Pages/BookingRules.razor.cs index e4adcf6..e2bf4da 100644 --- a/Schaad.Accounting.UI/Components/Pages/BookingRules.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/BookingRules.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Schaad.Accounting.Datasets; using Schaad.Accounting.Interfaces; @@ -11,21 +11,21 @@ public partial class BookingRules : ComponentBase { [Inject] private IViewService viewService { get; set; } = null!; - + [Inject] private IBookingRuleRepository bookingRuleRepository { get; set; } = null!; - + [Inject] private IDialogService dialogService { get; set; } = null!; - + private IQueryable? bookingRulesQueryable; - - protected override Task OnInitializedAsync() - { - bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable(); - return base.OnInitializedAsync(); + + protected override async Task OnInitializedAsync() + { + bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); + await base.OnInitializedAsync(); } - + private async Task AddAsync() { var data = new BookingRule(); @@ -40,13 +40,13 @@ public partial class BookingRules : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable(); + bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); } } - + private async Task EditAsync(string id) { - var data = bookingRuleRepository.GetBookingRule(id); + var data = await bookingRuleRepository.GetBookingRuleAsync(id); var dialog = await dialogService.ShowDialogAsync(data, new DialogParameters() { Height = "500px", @@ -58,19 +58,19 @@ public partial class BookingRules : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable(); + bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); } } - + private async Task DeleteAsync(string id) { - var bookingRule = bookingRuleRepository.GetBookingRule(id); + var bookingRule = await bookingRuleRepository.GetBookingRuleAsync(id); var dialog = await dialogService.ShowConfirmationAsync($"Regel '{bookingRule.LookupText}' wirklich löschen?", "Ja", "Nein", "Buchungsregel löschen"); var result = await dialog.Result; if (!result.Cancelled) { - bookingRuleRepository.DeleteBookingRule(id); - bookingRulesQueryable = viewService.GetBookingRuleViewList().AsQueryable(); + await bookingRuleRepository.DeleteBookingRuleAsync(id); + bookingRulesQueryable = (await viewService.GetBookingRuleViewListAsync()).AsQueryable(); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.UI/Components/Pages/BookingTexts.razor.cs b/Schaad.Accounting.UI/Components/Pages/BookingTexts.razor.cs index 3eb810c..df04bb8 100644 --- a/Schaad.Accounting.UI/Components/Pages/BookingTexts.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/BookingTexts.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -10,18 +10,18 @@ public partial class BookingTexts : ComponentBase { [Inject] private IBookingTextRepository bookingTextRepository { get; set; } = null!; - + [Inject] private IDialogService dialogService { get; set; } = null!; - + private IQueryable? bookingTextQueryable; - - protected override Task OnInitializedAsync() - { - bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable(); - return base.OnInitializedAsync(); + + protected override async Task OnInitializedAsync() + { + bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); + await base.OnInitializedAsync(); } - + private async Task AddAsync() { var data = new BookingText(); @@ -36,13 +36,13 @@ public partial class BookingTexts : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable(); + bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); } } - + private async Task EditAsync(string id) { - var data = bookingTextRepository.GetBookingText(id); + var data = await bookingTextRepository.GetBookingTextAsync(id); var dialog = await dialogService.ShowDialogAsync(data, new DialogParameters() { Height = "500px", @@ -54,19 +54,19 @@ public partial class BookingTexts : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable(); + bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); } } private async Task DeleteAsync(string id) { - var bookingText = bookingTextRepository.GetBookingText(id); + var bookingText = await bookingTextRepository.GetBookingTextAsync(id); var dialog = await dialogService.ShowConfirmationAsync($"Buchungstext '{bookingText.Text}' wirklich löschen?", "Ja", "Nein", "Buchungstext löschen"); var result = await dialog.Result; if (!result.Cancelled) { - bookingTextRepository.DeleteBookingText(id); - bookingTextQueryable = bookingTextRepository.GetBookingTextList().AsQueryable(); + await bookingTextRepository.DeleteBookingTextAsync(id); + bookingTextQueryable = (await bookingTextRepository.GetBookingTextListAsync()).AsQueryable(); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor b/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor index de222eb..2dee1dd 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor +++ b/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor @@ -3,4 +3,7 @@ Ausgaben - \ No newline at end of file +@if (config is not null && layout is not null && data is not null) +{ + +} \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor.cs b/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor.cs index bcc8b8f..8b0747f 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Charts/Assets.razor.cs @@ -8,14 +8,14 @@ namespace Schaad.Accounting.UI.Components.Pages.Charts; public partial class Assets : ComponentBase { private PlotlyChart chart = null!; - private Config config = null!; - private Plotly.Blazor.Layout layout = null!; - private IList data = null!; + private Config? config; + private Plotly.Blazor.Layout? layout; + private IList? data; [Inject] private IViewService viewService { get; set; } = null!; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { config = new Config { @@ -25,8 +25,8 @@ public partial class Assets : ComponentBase layout = new Plotly.Blazor.Layout { }; - - var accounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Activa); + + var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Activa); var values = new List(); var labels = new List(); var ids = new List(); @@ -50,6 +50,6 @@ public partial class Assets : ComponentBase } }; - return base.OnInitializedAsync(); + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor b/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor index 19412ef..64566a5 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor +++ b/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor @@ -3,4 +3,7 @@ Ausgaben - \ No newline at end of file +@if (config is not null && layout is not null && data is not null) +{ + +} \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor.cs b/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor.cs index 89bb8ae..6ab31f6 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Charts/Spendings.razor.cs @@ -8,14 +8,14 @@ namespace Schaad.Accounting.UI.Components.Pages.Charts; public partial class Spendings : ComponentBase { private PlotlyChart chart = null!; - private Config config = null!; - private Plotly.Blazor.Layout layout = null!; - private IList data = null!; + private Config? config; + private Plotly.Blazor.Layout? layout; + private IList? data; [Inject] private IViewService viewService { get; set; } = null!; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { config = new Config { @@ -25,8 +25,8 @@ public partial class Spendings : ComponentBase layout = new Plotly.Blazor.Layout { }; - - var accounts = viewService.GetAccountViewList().Where(a => a.Class == ClassIds.Expenses); + + var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses); var values = new List(); var labels = new List(); foreach (var grp in accounts.GroupBy(a => a.SubClass).Select(a => new {Key = a.Key, List = a.ToList()})) @@ -47,6 +47,6 @@ public partial class Spendings : ComponentBase } }; - return base.OnInitializedAsync(); + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor b/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor index 15988d6..8f8960e 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor +++ b/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor @@ -3,4 +3,7 @@ Ausgaben im Verlauf - \ No newline at end of file +@if (config is not null && layout is not null) +{ + +} \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor.cs b/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor.cs index 4a1e785..abd7497 100644 --- a/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Charts/SpendingsOverTime.razor.cs @@ -9,14 +9,14 @@ namespace Schaad.Accounting.UI.Components.Pages.Charts; public partial class SpendingsOverTime : ComponentBase { private PlotlyChart chart = null!; - private Config config = null!; - private Plotly.Blazor.Layout layout = null!; + private Config? config; + private Plotly.Blazor.Layout? layout; private IList data = new List(); [Inject] private IChartService chartService { get; set; } = null!; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { config = new Config { @@ -44,7 +44,7 @@ public partial class SpendingsOverTime : ComponentBase }; - var dataSeries = chartService.GetExpensesPerMonth(); + var dataSeries = await chartService.GetExpensesPerMonthAsync(); foreach (var dataSerie in dataSeries) { data.Add(new Bar @@ -55,6 +55,6 @@ public partial class SpendingsOverTime : ComponentBase }); }; - return base.OnInitializedAsync(); + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Classes.razor.cs b/Schaad.Accounting.UI/Components/Pages/Classes.razor.cs index bba7d92..d8464f0 100644 --- a/Schaad.Accounting.UI/Components/Pages/Classes.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Classes.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; @@ -10,18 +10,18 @@ public partial class Classes : ComponentBase { [Inject] private ISubclassRepository subclassRepository { get; set; } = null!; - + [Inject] private IDialogService dialogService { get; set; } = null!; - + private IQueryable? subclassQueryable; - - protected override Task OnInitializedAsync() - { - subclassQueryable = subclassRepository.GetSubClassList().AsQueryable(); - return base.OnInitializedAsync(); + + protected override async Task OnInitializedAsync() + { + subclassQueryable = (await subclassRepository.GetSubClassListAsync()).AsQueryable(); + await base.OnInitializedAsync(); } - + private async Task AddAsync() { var data = new SubClass(); @@ -36,13 +36,13 @@ public partial class Classes : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - subclassQueryable = subclassRepository.GetSubClassList().AsQueryable(); + subclassQueryable = (await subclassRepository.GetSubClassListAsync()).AsQueryable(); } } - + private async Task EditAsync(string id) { - var data = subclassRepository.GetSubClass(id); + var data = await subclassRepository.GetSubClassAsync(id); var dialog = await dialogService.ShowDialogAsync(data, new DialogParameters() { Height = "500px", @@ -54,19 +54,19 @@ public partial class Classes : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - subclassQueryable = subclassRepository.GetSubClassList().AsQueryable(); + subclassQueryable = (await subclassRepository.GetSubClassListAsync()).AsQueryable(); } } - + private async Task DeleteAsync(string id) { - var subclass = subclassRepository.GetSubClass(id); + var subclass = await subclassRepository.GetSubClassAsync(id); var dialog = await dialogService.ShowConfirmationAsync($"Klasse '{subclass.Name}' wirklich löschen?", "Ja", "Nein", "Klasse löschen"); var result = await dialog.Result; if (!result.Cancelled) { - subclassRepository.DeleteSubClass(id); - subclassQueryable = subclassRepository.GetSubClassList().AsQueryable(); + await subclassRepository.DeleteSubClassAsync(id); + subclassQueryable = (await subclassRepository.GetSubClassListAsync()).AsQueryable(); } } -} \ No newline at end of file +} diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/AccountDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/AccountDialog.razor.cs index bc949c8..22019ea 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/AccountDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/AccountDialog.razor.cs @@ -28,7 +28,7 @@ public partial class AccountDialog : ComponentBase { if (editContext.Validate()) { - accountRepository.SaveAccount(Content); + await accountRepository.SaveAccountAsync(Content); await Dialog.CloseAsync(Content); } } diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor index a7227d7..c6e408f 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor @@ -12,6 +12,12 @@ + @if (editContext is null) + { +

Lädt…

+ return; + } + diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor.cs index 7c7c1d5..84b4067 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingRuleDialog.razor.cs @@ -8,7 +8,7 @@ namespace Schaad.Accounting.UI.Components.Pages.Dialogs; public partial class BookingRuleDialog : ComponentBase { - private EditContext editContext = null!; + private EditContext? editContext; [CascadingParameter] public FluentDialog Dialog { get; set; } = null!; @@ -24,17 +24,17 @@ public partial class BookingRuleDialog : ComponentBase private IReadOnlyList accounts = []; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { editContext = new EditContext(Content); - accounts = viewService.GetAccountViewList(); + accounts = await viewService.GetAccountViewListAsync(); } private async Task SaveAsync() { if (editContext.Validate()) { - bookingRuleRepository.SaveBookingRule(Content); + await bookingRuleRepository.SaveBookingRuleAsync(Content); await Dialog.CloseAsync(Content); } } diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingTextDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingTextDialog.razor.cs index bb06aab..873720b 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingTextDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/BookingTextDialog.razor.cs @@ -28,7 +28,7 @@ public partial class BookingTextDialog : ComponentBase { if (editContext.Validate()) { - bookingTextRepository.SaveBookingText(Content); + await bookingTextRepository.SaveBookingTextAsync(Content); await Dialog.CloseAsync(Content); } } diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/ClassDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/ClassDialog.razor.cs index 31dc84d..b2c073d 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/ClassDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/ClassDialog.razor.cs @@ -28,7 +28,7 @@ public partial class ClassDialog : ComponentBase { if (editContext.Validate()) { - bookingRuleRepository.SaveSubClass(Content); + await bookingRuleRepository.SaveSubClassAsync(Content); await Dialog.CloseAsync(Content); } } diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor index ed322ad..427959a 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor @@ -11,6 +11,12 @@ + @if (editContext is null) + { +

Lädt…

+ return; + } + diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor.cs index 716fbc0..2bdc942 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionDialog.razor.cs @@ -8,7 +8,7 @@ namespace Schaad.Accounting.UI.Components.Pages.Dialogs; public partial class TransactionDialog : ComponentBase { - private EditContext editContext = null!; + private EditContext? editContext; [CascadingParameter] public FluentDialog Dialog { get; set; } = null!; @@ -25,11 +25,11 @@ public partial class TransactionDialog : ComponentBase private IReadOnlyList accounts = []; private DateTime? SelectedValue; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { SelectedValue = Content.ValueDate; editContext = new EditContext(Content); - accounts = viewService.GetAccountViewList(); + accounts = await viewService.GetAccountViewListAsync(); } private async Task SaveAsync() @@ -38,7 +38,7 @@ public partial class TransactionDialog : ComponentBase { Content.ValueDate = SelectedValue!.Value; Content.BookingDate = SelectedValue!.Value; - transactionRepository.SaveTransaction(Content); + await transactionRepository.SaveTransactionAsync(Content); await Dialog.CloseAsync(Content); } } diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor index 03e7bc9..db2a73c 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor @@ -11,6 +11,12 @@ + @if (editContext is null) + { +

Lädt…

+ return; + } + Total: @Math.Abs(Content.Value) / Rest: @openAmount diff --git a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor.cs b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor.cs index 25d0925..a3df0f1 100644 --- a/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Dialogs/TransactionSplitDialog.razor.cs @@ -9,7 +9,7 @@ namespace Schaad.Accounting.UI.Components.Pages.Dialogs; public partial class TransactionSplitDialog : ComponentBase { - private EditContext editContext = null!; + private EditContext? editContext; [CascadingParameter] public FluentDialog Dialog { get; set; } = null!; @@ -33,17 +33,17 @@ public partial class TransactionSplitDialog : ComponentBase private IViewService viewService { get; set; } = null!; private List transactionList = new(); - private IReadOnlyList accounts = null!; + private IReadOnlyList accounts = []; private IReadOnlyList bookingTexts = []; private decimal openAmount = 0; - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - accounts = viewService.GetAccountViewList(); - bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray(); - var accountList = accountRepository.GetAccountList(); - var bankTrx = bankTransactionRepository.GetBankTransaction(Content.BankTransactionId); + accounts = await viewService.GetAccountViewListAsync(); + bookingTexts = (await bookingTextRepository.GetBookingTextListAsync()).Select(b => b.Text).ToArray(); + var accountList = await accountRepository.GetAccountListAsync(); + var bankTrx = await bankTransactionRepository.GetBankTransactionAsync(Content.BankTransactionId); var trx = new Transaction(bankTrx, accountList); trx.Value = Math.Abs(bankTrx.Value); transactionList.Add(trx); @@ -74,7 +74,7 @@ public partial class TransactionSplitDialog : ComponentBase { foreach (var transtaction in transactionList) { - transactionRepository.SaveTransaction(transtaction); + await transactionRepository.SaveTransactionAsync(transtaction); } await Dialog.CloseAsync(Content); } diff --git a/Schaad.Accounting.UI/Components/Pages/Home.razor.cs b/Schaad.Accounting.UI/Components/Pages/Home.razor.cs index f203894..47060ad 100644 --- a/Schaad.Accounting.UI/Components/Pages/Home.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Home.razor.cs @@ -44,12 +44,12 @@ public partial class Home : ComponentBase bool IsCanceled; Dictionary Files = new(); - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - matchingBankTransactions = viewService.MatchOpenBankTransactions(); - accounts = viewService.GetAccountViewList(); - bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray(); - return base.OnInitializedAsync(); + matchingBankTransactions = await viewService.MatchOpenBankTransactionsAsync(); + accounts = await viewService.GetAccountViewListAsync(); + bookingTexts = (await bookingTextRepository.GetBookingTextListAsync()).Select(b => b.Text).ToArray(); + await base.OnInitializedAsync(); } async Task OnProgressChangeAsync(FluentInputFileEventArgs file) @@ -95,13 +95,13 @@ public partial class Home : ComponentBase File.Delete(file.Value); } - - matchingBankTransactions = viewService.MatchOpenBankTransactions(); + + matchingBankTransactions = await viewService.MatchOpenBankTransactionsAsync(); } private async Task ImportXmlAndShowResultAsync(string fileName) { - var messages = fileService.ImportAccountStatementFile(fileName); + var messages = await fileService.ImportAccountStatementFileAsync(fileName); foreach (var message in messages) { await ShowImportResultAsync(message); @@ -156,9 +156,8 @@ public partial class Home : ComponentBase ShowToast("Transaktion kann wegen fehlenden Daten nicht gespeichert werden!", ToastIntent.Error); return; } - transactionRepository.SaveTransaction(transaction); + await transactionRepository.SaveTransactionAsync(transaction); transaction.ReadOnly = true; - await Task.CompletedTask; } private async Task SplitBankTransactionAsync(Transaction transaction) diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor index 13b1fd6..74e2a17 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor +++ b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor @@ -1,5 +1,11 @@ @page "/BalanceReport" +@if (balance is null) +{ +

Lädt…

+ return; +} +

@header

Print diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor.cs b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor.cs index 9df72cf..9a749ad 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceReport.razor.cs @@ -12,17 +12,17 @@ public partial class BalanceReport : ComponentBase [Inject] private ISettingsService settingsService { get; set; } = null!; - private BalanceDataset balance = null!; + private BalanceDataset? balance; private decimal win; - private string header = null!; - private string footer = null!; + private string header = ""; + private string footer = ""; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - balance = viewService.GetBalanceView(); + balance = await viewService.GetBalanceViewAsync(); win = balance.TotalActivaCHF - balance.TotalPassivaCHF; (header, footer) = Report.GetViewDataTitleAndFooter("Bilanz", settingsService); - - return base.OnInitializedAsync(); + + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor index 80ec629..d015063 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor +++ b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor @@ -1,5 +1,11 @@ @page "/BalanceSheetReport" +@if (balanceSheet is null) +{ +

Lädt…

+ return; +} +

@header

Print
diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor.cs b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor.cs index 35422b5..d97b851 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Reports/BalanceSheetReport.razor.cs @@ -12,17 +12,17 @@ public partial class BalanceSheetReport : ComponentBase [Inject] private ISettingsService settingsService { get; set; } = null!; - private BalanceSheetDataset balanceSheet = null!; + private BalanceSheetDataset? balanceSheet; private decimal win; - private string header = null!; - private string footer = null!; + private string header = ""; + private string footer = ""; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - balanceSheet = viewService.GetBalanceSheetView(settingsService.GetYear()); + balanceSheet = await viewService.GetBalanceSheetViewAsync(settingsService.GetYear()); win = balanceSheet.ProfitCHF - balanceSheet.LossCHF; (header, footer) = Report.GetViewDataTitleAndFooter("Jahresabschluss", settingsService); - - return base.OnInitializedAsync(); + + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor b/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor index 95a8d5b..7939662 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor +++ b/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor @@ -1,5 +1,11 @@ @page "/DetailReport" +@if (accounts is null || transactions is null) +{ +

Lädt…

+ return; +} +

@header

Print diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor.cs b/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor.cs index 45b0590..c914f74 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Reports/DetailReport.razor.cs @@ -12,23 +12,23 @@ public partial class DetailReport : ComponentBase [Inject] private ISettingsService settingsService { get; set; } = null!; - private IReadOnlyList accounts = null!; - private IReadOnlyList transactions = null!; + private IReadOnlyList? accounts; + private IReadOnlyList? transactions; private decimal profit; private decimal loss; - private string header = null!; - private string footer = null!; + private string header = ""; + private string footer = ""; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - accounts = viewService.GetAccountViewList(); - transactions = viewService.GetTransactionViewList(); - - profit = Math.Abs(accounts.Where(m => m.Class == 3).Sum(m => m.Balance)); - loss = Math.Abs(accounts.Where(m => m.Class == 4).Sum(m => m.Balance)); - + accounts = await viewService.GetAccountViewListAsync(); + transactions = await viewService.GetTransactionViewListAsync(); + + profit = Math.Abs(accounts.Where(m => m.Class == ClassIds.Income).Sum(m => m.Balance)); + loss = Math.Abs(accounts.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.Balance)); + (header, footer) = Report.GetViewDataTitleAndFooter("Detailaufstellung", settingsService); - - return base.OnInitializedAsync(); + + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor b/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor index acfb8d2..7cc6666 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor +++ b/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor @@ -1,5 +1,11 @@ @page "/ProfitLossReport" +@if (accounts is null) +{ +

Lädt…

+ return; +} +

@header

Print diff --git a/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor.cs b/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor.cs index 0034057..c7c4217 100644 --- a/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Reports/ProfitLossReport.razor.cs @@ -13,22 +13,22 @@ public partial class ProfitLossReport : ComponentBase [Inject] private ISettingsService settingsService { get; set; } = null!; - private IReadOnlyList accounts = null!; + private IReadOnlyList? accounts; private decimal profit; private decimal loss; private decimal win; - private string header = null!; - private string footer = null!; + private string header = ""; + private string footer = ""; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - accounts = viewService.GetAccountViewList(); + accounts = await viewService.GetAccountViewListAsync(); profit = Math.Abs(accounts.Where(m => m.Class == ClassIds.Income).Sum(m => m.Balance)); loss = Math.Abs(accounts.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.Balance)); - win = profit-loss; + win = profit - loss; (header, footer) = Report.GetViewDataTitleAndFooter("Erfolgsrechnung", settingsService); - - return base.OnInitializedAsync(); + + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Schaad.Accounting.UI/Components/Pages/Transactions.razor b/Schaad.Accounting.UI/Components/Pages/Transactions.razor index 38f0c02..b473ac8 100644 --- a/Schaad.Accounting.UI/Components/Pages/Transactions.razor +++ b/Schaad.Accounting.UI/Components/Pages/Transactions.razor @@ -10,7 +10,7 @@ @foreach (var account in gr) { - +
@account.Number / @account.Name @@ -29,36 +29,39 @@ - - - @selectedAccount.Name - - - Total @Math.Abs(selectedAccount.Balance).ToString("N2") @selectedAccount.Currency in @transactionList?.Count() Transaktionen - - - Neue Transaktion hinzufügen - - - - - - - - - - @GetAccountName(context) - - - @context.GetValue(selectedAccount.IsFxAccount).ToString("N2") @selectedAccount.Currency - - - - - - - - + @if (selectedAccount is not null) + { + + + @selectedAccount.Name + + + Total @Math.Abs(selectedAccount.Balance).ToString("N2") @selectedAccount.Currency in @transactionList?.Count() Transaktionen + + + Neue Transaktion hinzufügen + + + + + + + + + + @GetAccountName(context) + + + @context.GetValue(selectedAccount.IsFxAccount).ToString("N2") @selectedAccount.Currency + + + + + + + + + } diff --git a/Schaad.Accounting.UI/Components/Pages/Transactions.razor.cs b/Schaad.Accounting.UI/Components/Pages/Transactions.razor.cs index ffc0c98..75aa933 100644 --- a/Schaad.Accounting.UI/Components/Pages/Transactions.razor.cs +++ b/Schaad.Accounting.UI/Components/Pages/Transactions.razor.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components; using Schaad.Accounting.Datasets; using Schaad.Accounting.Interfaces; @@ -11,33 +11,33 @@ public partial class Transactions : ComponentBase { [Inject] private IViewService viewService { get; set; } = null!; - + [Inject] private ITransactionRepository transactionRepository { get; set; } = null!; - + [Inject] private IDialogService dialogService { get; set; } = null!; - private IReadOnlyList accounts = null!; - + private IReadOnlyList accounts = []; + private IQueryable? transactionList; - private string selectedAccountId = null!; - private AccountDataset selectedAccount = null!; - - protected override Task OnInitializedAsync() - { - accounts = viewService.GetAccountViewList(); - ShowTransactions(accounts.First().Id); - return base.OnInitializedAsync(); + private string? selectedAccountId; + private AccountDataset? selectedAccount; + + protected override async Task OnInitializedAsync() + { + accounts = await viewService.GetAccountViewListAsync(); + await ShowTransactionsAsync(accounts.First().Id); + await base.OnInitializedAsync(); } - private void ShowTransactions(string accountId) + private async Task ShowTransactionsAsync(string accountId) { selectedAccountId = accountId; selectedAccount = accounts.Single(a => a.Id == accountId); - transactionList = viewService.GetTransactionViewList(accountId).AsQueryable(); + transactionList = (await viewService.GetTransactionViewListAsync(accountId)).AsQueryable(); } - + private string GetAccountName(TransactionDataset transaction) { if (selectedAccountId == transaction.OriginAccountId) @@ -47,7 +47,7 @@ public partial class Transactions : ComponentBase return transaction.OriginAccount.Name; } - + private async Task AddAsync() { var data = new Transaction @@ -68,13 +68,13 @@ public partial class Transactions : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable(); + transactionList = (await viewService.GetTransactionViewListAsync(selectedAccountId)).AsQueryable(); } } - + private async Task EditAsync(string id) { - var data = transactionRepository.GetTransaction(id); + var data = await transactionRepository.GetTransactionAsync(id); var dialog = await dialogService.ShowDialogAsync(data, new DialogParameters() { Height = "500px", @@ -86,19 +86,19 @@ public partial class Transactions : ComponentBase var result = await dialog.Result; if (!result.Cancelled && result.Data != null) { - transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable(); + transactionList = (await viewService.GetTransactionViewListAsync(selectedAccountId)).AsQueryable(); } } - + private async Task DeleteAsync(string id) { - var transaction = transactionRepository.GetTransaction(id); + var transaction = await transactionRepository.GetTransactionAsync(id); var dialog = await dialogService.ShowConfirmationAsync($"Transaction '{transaction.Text}' mit Betrag {transaction.Value} wirklich löschen?", "Ja", "Nein", "Transaktion löschen"); var result = await dialog.Result; if (!result.Cancelled) { - transactionRepository.DeleteTransaction(id); - transactionList = viewService.GetTransactionViewList(selectedAccountId).AsQueryable(); + await transactionRepository.DeleteTransactionAsync(id); + transactionList = (await viewService.GetTransactionViewListAsync(selectedAccountId)).AsQueryable(); } } -} \ No newline at end of file +}