Compare commits

..

No commits in common. "786fafefa56b5cacb359518698366152799ebb34" and "8926fb1e973ba6629717768432a991dc521d72a1" have entirely different histories.

36 changed files with 178 additions and 270 deletions

View file

@ -103,10 +103,3 @@ Analysis and phased plan produced 2026-07-02. See conversation history for full
- **PR F** — Phase 3 item 21: precomputed dictionaries in `ViewService` to eliminate O(N·M) `Single(...)` scans and per-account transaction filtering. - **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)** — 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.
- **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 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<FileService>` 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` `<Reference>` from `Common.csproj` — Common is now vendor-free and matches its documented role as the "shared models, DTOs, interfaces" layer.
- **PR J** — Phase 2 item 18 (second slice): thread `ILogger<T>` through `BaseRepository` and every concrete repository so `BaseRepository.Save`'s catch block logs the failing file path and exception before rethrowing (previously the exception's origin was silently swallowed and only the stack trace at the callsite survived). Pulls `Microsoft.Extensions.Logging.Abstractions` into the Db project. `TransactionRepositoryTestShould` and `AccountRepositoryTestShould` use `NullLogger<T>.Instance`. Remaining item 18 work (MatchOpenBankTransactions match-count summary in `ViewService`) still open.
- **PR K** — Phase 5 item 27: introduce `IFxConverter.ConvertToChf(amount, fromCurrency)` and its `FxConverter` implementation. The vendor `IFxService` and the FixerIo API key are now hidden inside `FxConverter`; callers stop threading the API key through every method call. `ViewService` drops both `IFxService` and `ISettingsService` from its constructor and takes `IFxConverter` instead. Simplifies `GetAccountViewList` (no more per-call `settingsService.GetSettings()` reads) and `GetBalanceSheetView`. Two-test `FxConverterTestShould` locks in the target-currency and API-key routing.
- **PR L** — Phase 2 item 10: drop the hand-rolled `Copy(target)` methods from `Account`, `BookingRule`, `BookingText`, `SubClass`, and `SplitPredefinition`; keep only a `Transaction.Clone()` for defensive-copy needs in `GetTransaction` / `ViewService.WithDisplaySign`. Every `SaveXxx` now uses `FindIndex` → in-place replace (or `Add` for new entries), moving the two "hidden" defaults (`Account.Currency = "CHF"` when unset, `Transaction.BookingDate = ValueDate` when unset) into the corresponding `SaveXxx` method where they belong. Fixes a pre-existing bug: `Transaction.Copy` never copied `RelatedParty`, so update-saves silently dropped it. Added `PreserveRelatedPartyWhenRoundTrippingTransaction` and `DefaultBookingDateToValueDateWhenBookingDateIsUnset` as regression tests. 42 tests total.
- **PR M** — Phase 2 item 18 (final slice): add `ILogger<ViewService>` and log a match-count summary from `MatchOpenBankTransactions` (`"Matched {Matched} of {Total} open bank transactions"`). Completes item 18 — statement imports (PR H), repository save failures (PR J), and match runs now all emit structured logs. Also removes a dead `ISettingsService` injection from `Home.razor.cs` (declared with `[Inject]` but never used anywhere in the file).
- **PR 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`.

View file

@ -1,6 +1,6 @@
namespace Schaad.Accounting namespace Schaad.Accounting
{ {
public static class ClassIds public class ClassIds
{ {
public const int Activa = 1; public const int Activa = 1;
public const int Passiva = 2; public const int Passiva = 2;

View file

@ -5,10 +5,5 @@
public string DataPath { get; set; } public string DataPath { get; set; }
public string FixerIoApiKey { get; set; } public string FixerIoApiKey { get; set; }
/// <summary>
/// Mandator selected when the app starts. Users can switch via the header dropdown.
/// </summary>
public string DefaultMandator { get; set; } = "Claudio Schaad";
} }
} }

View file

@ -59,5 +59,19 @@ namespace Schaad.Accounting.Models
{ {
get { return string.IsNullOrEmpty(Currency) == false && Currency != "CHF"; } get { return string.IsNullOrEmpty(Currency) == false && Currency != "CHF"; }
} }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(Account target)
{
target.LastBankBalance = LastBankBalance;
target.StartBalance = StartBalance;
target.BankAccountNumber = BankAccountNumber;
target.Currency = string.IsNullOrEmpty(Currency) ? "CHF" : Currency;
target.Id = Id;
target.Name = Name;
target.Number = Number;
}
} }
} }

View file

@ -20,5 +20,18 @@ namespace Schaad.Accounting.Models
[Display(Name = "Konto")] [Display(Name = "Konto")]
[Required] [Required]
public string AccountId { get; set; } public string AccountId { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(BookingRule target)
{
target.LookupText = LookupText;
target.LookupValue = LookupValue;
target.BookingText = BookingText;
target.Id = Id;
target.AccountId = AccountId;
}
} }
} }

View file

@ -10,5 +10,14 @@ namespace Schaad.Accounting.Models
[Required] [Required]
[MinLength(3)] [MinLength(3)]
public string Text { get; set; } public string Text { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(BookingText target)
{
target.Id = Id;
target.Text = Text;
}
} }
} }

View file

@ -9,5 +9,17 @@
public decimal BookingValue { get; set; } public decimal BookingValue { get; set; }
public string AccountId { get; set; } public string AccountId { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(SplitPredefinition target)
{
target.BookingText = BookingText;
target.BookingValue = BookingValue;
target.Id = Id;
target.AccountId = AccountId;
}
} }
} }

View file

@ -14,5 +14,15 @@ namespace Schaad.Accounting.Models
[Required] [Required]
[MinLength(3)] [MinLength(3)]
public string Name { get; set; } public string Name { get; set; }
/// <summary>
/// Makes a copy
/// </summary>
public void Copy(SubClass target)
{
target.Id = Id;
target.Name = Name;
target.Number = Number;
}
} }
} }

View file

@ -74,22 +74,20 @@ namespace Schaad.Accounting.Models
} }
/// <summary> /// <summary>
/// Returns a shallow independent copy — used to hand out defensive copies /// Makes a copy
/// (e.g. from repository reads) without exposing the caller to later mutation.
/// </summary> /// </summary>
public Transaction Clone() => new() public void Copy(Transaction target)
{ {
Id = Id, target.BankTransactionId = BankTransactionId;
BankTransactionId = BankTransactionId, target.BankTransactionText = BankTransactionText;
BankTransactionText = BankTransactionText, target.Id = Id;
RelatedParty = RelatedParty, target.OriginAccountId = OriginAccountId;
OriginAccountId = OriginAccountId, target.TargetAccountId = TargetAccountId;
TargetAccountId = TargetAccountId, target.Text = Text;
Text = Text, target.Value = Value;
Value = Value, target.ValueDate = ValueDate;
ValueDate = ValueDate, target.BookingDate = BookingDate > DateTime.MinValue ? BookingDate : ValueDate;
BookingDate = BookingDate, target.FxRate = FxRate;
FxRate = FxRate }
};
} }
} }

View file

@ -4,4 +4,9 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RootNamespace>Schaad.Accounting</RootNamespace> <RootNamespace>Schaad.Accounting</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Reference Include="Schaad.Finance.Api">
<HintPath>..\dlls\Schaad.Finance.Api.dll</HintPath>
</Reference>
</ItemGroup>
</Project> </Project>

View file

@ -2,7 +2,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -12,8 +11,8 @@ namespace Schaad.Accounting.Repositories
{ {
private readonly string ACCOUNTS = "Accounts.xml"; private readonly string ACCOUNTS = "Accounts.xml";
public AccountRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<AccountRepository> logger) public AccountRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
EnsureAccountsFile(); EnsureAccountsFile();
} }
@ -28,22 +27,16 @@ namespace Schaad.Accounting.Repositories
/// </summary> /// </summary>
public void SaveAccount(Account account) public void SaveAccount(Account account)
{ {
if (string.IsNullOrEmpty(account.Currency))
{
account.Currency = "CHF";
}
var accounts = GetAccountList(); var accounts = GetAccountList();
var idx = accounts.FindIndex(a => a.Id == account.Id); var existingAccount = accounts.FirstOrDefault(a => a.Id == account.Id);
if (idx >= 0)
{ if (existingAccount == null)
accounts[idx] = account;
}
else
{ {
existingAccount = new Account();
accounts.Add(existingAccount);
account.Id = Guid.NewGuid().ToString(); account.Id = Guid.NewGuid().ToString();
accounts.Add(account);
} }
account.Copy(existingAccount);
Save(accounts, ACCOUNTS); Save(accounts, ACCOUNTS);
} }

View file

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -11,8 +10,8 @@ namespace Schaad.Accounting.Repositories
{ {
private readonly string BANK_TRANSACTIONS = "BankTransactions.xml"; private readonly string BANK_TRANSACTIONS = "BankTransactions.xml";
public BankTransactionRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<BankTransactionRepository> logger) public BankTransactionRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
} }

View file

@ -5,7 +5,6 @@ using System.IO;
using System.Text; using System.Text;
using System.Xml; using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
namespace Schaad.Accounting.Repositories namespace Schaad.Accounting.Repositories
@ -19,13 +18,11 @@ namespace Schaad.Accounting.Repositories
protected readonly ISettingsService settingsService; protected readonly ISettingsService settingsService;
protected readonly RepositoryCache cache; protected readonly RepositoryCache cache;
protected readonly ILogger logger;
protected BaseRepository(ISettingsService settingsService, RepositoryCache cache, ILogger logger) protected BaseRepository(ISettingsService settingsService, RepositoryCache cache)
{ {
this.settingsService = settingsService; this.settingsService = settingsService;
this.cache = cache; this.cache = cache;
this.logger = logger;
} }
protected void EnsureFileExists(string fileName) protected void EnsureFileExists(string fileName)
@ -82,9 +79,8 @@ namespace Schaad.Accounting.Repositories
File.Move(tmpPath, filePath, overwrite: true); File.Move(tmpPath, filePath, overwrite: true);
} }
catch (Exception ex) catch
{ {
logger.LogError(ex, "Failed to save {FilePath}", filePath);
if (File.Exists(tmpPath)) if (File.Exists(tmpPath))
{ {
try { File.Delete(tmpPath); } catch { /* best effort */ } try { File.Delete(tmpPath); } catch { /* best effort */ }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -11,8 +10,8 @@ namespace Schaad.Accounting.Repositories
{ {
private readonly string BOOKING_RULES = "BookingRules.xml"; private readonly string BOOKING_RULES = "BookingRules.xml";
public BookingRuleRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<BookingRuleRepository> logger) public BookingRuleRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
EnsureFileExists(BOOKING_RULES); EnsureFileExists(BOOKING_RULES);
} }
@ -28,16 +27,15 @@ namespace Schaad.Accounting.Repositories
public void SaveBookingRule(BookingRule bookingRule) public void SaveBookingRule(BookingRule bookingRule)
{ {
var bookingRules = GetBookingRuleList(); var bookingRules = GetBookingRuleList();
var idx = bookingRules.FindIndex(r => r.Id == bookingRule.Id); var existingRule = bookingRules.FirstOrDefault(a => a.Id == bookingRule.Id);
if (idx >= 0)
{ if (existingRule == null)
bookingRules[idx] = bookingRule;
}
else
{ {
existingRule = new BookingRule();
bookingRules.Add(existingRule);
bookingRule.Id = Guid.NewGuid().ToString(); bookingRule.Id = Guid.NewGuid().ToString();
bookingRules.Add(bookingRule);
} }
bookingRule.Copy(existingRule);
Save(bookingRules, BOOKING_RULES); Save(bookingRules, BOOKING_RULES);
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -11,8 +10,8 @@ namespace Schaad.Accounting.Repositories
{ {
private readonly string BOOKING_TEXTS = "BookingTexts.xml"; private readonly string BOOKING_TEXTS = "BookingTexts.xml";
public BookingTextRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<BookingTextRepository> logger) public BookingTextRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
EnsureFileExists(BOOKING_TEXTS); EnsureFileExists(BOOKING_TEXTS);
} }
@ -28,16 +27,15 @@ namespace Schaad.Accounting.Repositories
public void SaveBookingText(BookingText bookingText) public void SaveBookingText(BookingText bookingText)
{ {
var bookingTexts = GetBookingTextList(); var bookingTexts = GetBookingTextList();
var idx = bookingTexts.FindIndex(t => t.Id == bookingText.Id); var existingText = bookingTexts.FirstOrDefault(a => a.Id == bookingText.Id);
if (idx >= 0)
{ if (existingText == null)
bookingTexts[idx] = bookingText;
}
else
{ {
existingText = new BookingText();
bookingTexts.Add(existingText);
bookingText.Id = Guid.NewGuid().ToString(); bookingText.Id = Guid.NewGuid().ToString();
bookingTexts.Add(bookingText);
} }
bookingText.Copy(existingText);
Save(bookingTexts, BOOKING_TEXTS); Save(bookingTexts, BOOKING_TEXTS);
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -11,8 +10,8 @@ namespace Schaad.Accounting.Repositories
{ {
private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml"; private readonly string SPLIT_PREDEFINITION = "SplitPredefinitions.xml";
public SplitPredefinitionRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<SplitPredefinitionRepository> logger) public SplitPredefinitionRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
EnsureFileExists(SPLIT_PREDEFINITION); EnsureFileExists(SPLIT_PREDEFINITION);
} }
@ -28,16 +27,15 @@ namespace Schaad.Accounting.Repositories
public void SaveSplitPredefinition(SplitPredefinition splitPredefinition) public void SaveSplitPredefinition(SplitPredefinition splitPredefinition)
{ {
var definitions = GetSplitPredefinitionList(); var definitions = GetSplitPredefinitionList();
var idx = definitions.FindIndex(d => d.Id == splitPredefinition.Id); var existingDefinition = definitions.FirstOrDefault(a => a.Id == splitPredefinition.Id);
if (idx >= 0)
{ if (existingDefinition == null)
definitions[idx] = splitPredefinition;
}
else
{ {
existingDefinition = new SplitPredefinition();
definitions.Add(existingDefinition);
splitPredefinition.Id = Guid.NewGuid().ToString(); splitPredefinition.Id = Guid.NewGuid().ToString();
definitions.Add(splitPredefinition);
} }
splitPredefinition.Copy(existingDefinition);
Save(definitions, SPLIT_PREDEFINITION); Save(definitions, SPLIT_PREDEFINITION);
} }
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -12,8 +11,8 @@ namespace Schaad.Accounting.Repositories
private readonly Dictionary<int, string> classes = new Dictionary<int, string>(); private readonly Dictionary<int, string> classes = new Dictionary<int, string>();
private readonly string SUBCLASSES = "SubClasses.xml"; private readonly string SUBCLASSES = "SubClasses.xml";
public SubclassRepository(ISettingsService settingsService, RepositoryCache cache, ILogger<SubclassRepository> logger) public SubclassRepository(ISettingsService settingsService, RepositoryCache cache)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
EnsureFileExists(SUBCLASSES); EnsureFileExists(SUBCLASSES);
@ -29,21 +28,20 @@ namespace Schaad.Accounting.Repositories
public List<SubClass> GetSubClassList() => LoadList<SubClass>(SUBCLASSES); public List<SubClass> GetSubClassList() => LoadList<SubClass>(SUBCLASSES);
/// <summary> /// <summary>
/// Save a subclass (insert/update) /// Save a booking text (insert/update)
/// </summary> /// </summary>
public void SaveSubClass(SubClass subClass) public void SaveSubClass(SubClass subClass)
{ {
var subclasses = GetSubClassList(); var subclasses = GetSubClassList();
var idx = subclasses.FindIndex(s => s.Id == subClass.Id); var existingSubClass = subclasses.FirstOrDefault(a => a.Id == subClass.Id);
if (idx >= 0)
{ if (existingSubClass == null)
subclasses[idx] = subClass;
}
else
{ {
existingSubClass = new SubClass();
subclasses.Add(existingSubClass);
subClass.Id = Guid.NewGuid().ToString(); subClass.Id = Guid.NewGuid().ToString();
subclasses.Add(subClass);
} }
subClass.Copy(existingSubClass);
Save(subclasses, SUBCLASSES); Save(subclasses, SUBCLASSES);
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -12,8 +11,8 @@ namespace Schaad.Accounting.Repositories
private readonly IAccountRepository accountRepository; private readonly IAccountRepository accountRepository;
private readonly string TRANSACTIONS = "Transactions.xml"; private readonly string TRANSACTIONS = "Transactions.xml";
public TransactionRepository(ISettingsService settingsService, RepositoryCache cache, IAccountRepository accountRepository, ILogger<TransactionRepository> logger) public TransactionRepository(ISettingsService settingsService, RepositoryCache cache, IAccountRepository accountRepository)
: base(settingsService, cache, logger) : base(settingsService, cache)
{ {
this.accountRepository = accountRepository; this.accountRepository = accountRepository;
} }
@ -45,22 +44,17 @@ namespace Schaad.Accounting.Repositories
transaction.FxRate = null; transaction.FxRate = null;
} }
if (transaction.BookingDate <= DateTime.MinValue)
{
transaction.BookingDate = transaction.ValueDate;
}
var transactionList = GetTransactionList(); var transactionList = GetTransactionList();
var idx = transactionList.FindIndex(t => t.Id == transaction.Id); var existingTransaction = transactionList.FirstOrDefault(a => a.Id == transaction.Id);
if (idx >= 0)
{ if (existingTransaction == null)
transactionList[idx] = transaction;
}
else
{ {
existingTransaction = new Transaction();
transactionList.Add(existingTransaction);
transaction.Id = Guid.NewGuid().ToString(); transaction.Id = Guid.NewGuid().ToString();
transactionList.Add(transaction);
} }
transaction.Copy(existingTransaction);
Save(transactionList, TRANSACTIONS); Save(transactionList, TRANSACTIONS);
} }
@ -76,7 +70,8 @@ namespace Schaad.Accounting.Repositories
return null; return null;
} }
var result = stored.Clone(); var result = new Transaction();
stored.Copy(result);
// value is stored in CHF -> convert back to foreign currency for display/editing // value is stored in CHF -> convert back to foreign currency for display/editing
var isFxAccount = accountRepository.GetAccount(result.OriginAccountId).IsFxAccount var isFxAccount = accountRepository.GetAccount(result.OriginAccountId).IsFxAccount

View file

@ -7,8 +7,4 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" /> <ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
</ItemGroup>
</Project> </Project>

View file

@ -2,7 +2,6 @@ using System.Collections.Generic;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Finance.Api; using Schaad.Finance.Api;
@ -18,7 +17,6 @@ namespace Schaad.Accounting.Services
private readonly ISettingsService settingsService; private readonly ISettingsService settingsService;
private readonly IAccountStatementService accountStatementService; private readonly IAccountStatementService accountStatementService;
private readonly ICreditCardStatementService creditCardStatementService; private readonly ICreditCardStatementService creditCardStatementService;
private readonly ILogger<FileService> logger;
public FileService( public FileService(
ISettingsService settingsService, ISettingsService settingsService,
@ -26,8 +24,7 @@ namespace Schaad.Accounting.Services
ITransactionRepository transactionsRepository, ITransactionRepository transactionsRepository,
IBankTransactionRepository bankTransactionRepository, IBankTransactionRepository bankTransactionRepository,
IAccountStatementService accountStatementService, IAccountStatementService accountStatementService,
ICreditCardStatementService creditCardStatementService, ICreditCardStatementService creditCardStatementService)
ILogger<FileService> logger)
{ {
this.settingsService = settingsService; this.settingsService = settingsService;
this.accountRepository = accountRepository; this.accountRepository = accountRepository;
@ -35,7 +32,6 @@ namespace Schaad.Accounting.Services
this.bankTransactionRepository = bankTransactionRepository; this.bankTransactionRepository = bankTransactionRepository;
this.accountStatementService = accountStatementService; this.accountStatementService = accountStatementService;
this.creditCardStatementService = creditCardStatementService; this.creditCardStatementService = creditCardStatementService;
this.logger = logger;
} }
public string Backup() public string Backup()
@ -50,8 +46,6 @@ namespace Schaad.Accounting.Services
// http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6 // http://www.mikesdotnetting.com/article/288/asp-net-5-uploading-files-with-asp-net-mvc-6
public IReadOnlyList<MessageDataset> ImportAccountStatementFile(string filePath) public IReadOnlyList<MessageDataset> ImportAccountStatementFile(string filePath)
{ {
logger.LogInformation("Importing account statement file {FilePath}", filePath);
var messages = new List<MessageDataset>(); var messages = new List<MessageDataset>();
var accountStatementResults = accountStatementService.ReadFile(filePath, Encoding.UTF8); var accountStatementResults = accountStatementService.ReadFile(filePath, Encoding.UTF8);
@ -62,7 +56,6 @@ namespace Schaad.Accounting.Services
if (accountStatementResult.IsSuccess == false) if (accountStatementResult.IsSuccess == false)
{ {
logger.LogError("Statement parse failed for account {AccountNumber}: {Error}", account.AccountNumber, accountStatementResult.Error);
messages.Add(new MessageDataset($"Account {account.AccountNumber} NICHT importiert: {accountStatementResult.Error}", MessageStatus.Error)); messages.Add(new MessageDataset($"Account {account.AccountNumber} NICHT importiert: {accountStatementResult.Error}", MessageStatus.Error));
} }
@ -94,9 +87,6 @@ namespace Schaad.Accounting.Services
var count = bankTransactionRepository.SaveBankTransactionList(account.AccountNumber, transactionsThisYear); var count = bankTransactionRepository.SaveBankTransactionList(account.AccountNumber, transactionsThisYear);
accountRepository.SaveBankAccountBalance(account.AccountNumber, (decimal)account.EndBalance.Value); accountRepository.SaveBankAccountBalance(account.AccountNumber, (decimal)account.EndBalance.Value);
logger.LogInformation("Imported {Imported} of {Total} transactions for account {AccountNumber}",
count, account.Transactions.Count, account.AccountNumber);
var status = count == account.Transactions.Count ? MessageStatus.Success : MessageStatus.Info; var status = count == account.Transactions.Count ? MessageStatus.Success : MessageStatus.Info;
message.Add($"{count} von {account.Transactions.Count} Transaktion(en) importiert.", status); message.Add($"{count} von {account.Transactions.Count} Transaktion(en) importiert.", status);
@ -109,8 +99,6 @@ namespace Schaad.Accounting.Services
} }
else else
{ {
logger.LogWarning("Skipping account {AccountNumber}: not part of the currently selected mandator ({Skipped} transactions ignored)",
account.AccountNumber, account.Transactions.Count);
message.Add($"Falscher Mandant: {account.Transactions.Count} Transaktion(en) nicht importiert.", MessageStatus.Warning); message.Add($"Falscher Mandant: {account.Transactions.Count} Transaktion(en) nicht importiert.", MessageStatus.Warning);
} }
} }

View file

@ -1,21 +0,0 @@
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces;
using Schaad.Finance.Api;
namespace Schaad.Accounting.Services
{
public class FxConverter : IFxConverter
{
private readonly IFxService fxService;
private readonly SettingsDataset settings;
public FxConverter(IFxService fxService, SettingsDataset settings)
{
this.fxService = fxService;
this.settings = settings;
}
public decimal ConvertToChf(decimal amount, string fromCurrency)
=> fxService.ConvertCurrency(amount, fromCurrency, "CHF", settings.FixerIoApiKey);
}
}

View file

@ -1,11 +0,0 @@
namespace Schaad.Accounting.Interfaces
{
/// <summary>
/// Thin wrapper over the vendor <c>IFxService</c> that hides the FixerIo API key
/// and the base currency, so callers just ask "convert this amount to CHF".
/// </summary>
public interface IFxConverter
{
decimal ConvertToChf(decimal amount, string fromCurrency);
}
}

View file

@ -6,7 +6,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FreeSpire.PDF" Version="10.2.0" /> <PackageReference Include="FreeSpire.PDF" Version="10.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" /> <ProjectReference Include="..\Schaad.Accounting.Common\Schaad.Accounting.Common.csproj" />

View file

@ -10,13 +10,12 @@ namespace Schaad.Accounting.Services
public class SettingsService : ISettingsService public class SettingsService : ISettingsService
{ {
private readonly SettingsDataset settingsDataset; private readonly SettingsDataset settingsDataset;
private string mandator; private string mandator = "Claudio Schaad";
private int year = DateTime.Now.Year; private int year = DateTime.Now.Year;
public SettingsService(SettingsDataset settingsDataset) public SettingsService(SettingsDataset settingsDataset)
{ {
this.settingsDataset = settingsDataset; this.settingsDataset = settingsDataset;
this.mandator = settingsDataset.DefaultMandator;
} }
public int GetYear() public int GetYear()

View file

@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
using Schaad.Accounting.Datasets; using Schaad.Accounting.Datasets;
using Schaad.Accounting.Datasets.Reports; using Schaad.Accounting.Datasets.Reports;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
using Schaad.Finance.Api;
using Schaad.Finance.Api.Datasets; using Schaad.Finance.Api.Datasets;
namespace Schaad.Accounting.Services namespace Schaad.Accounting.Services
@ -17,8 +17,8 @@ namespace Schaad.Accounting.Services
private readonly IBookingRuleRepository bookingRuleRepository; private readonly IBookingRuleRepository bookingRuleRepository;
private readonly ISubclassRepository subclassRepository; private readonly ISubclassRepository subclassRepository;
private readonly ITransactionRepository transactionRepository; private readonly ITransactionRepository transactionRepository;
private readonly IFxConverter fxConverter; private readonly IFxService fxService;
private readonly ILogger<ViewService> logger; private readonly ISettingsService settingsService;
public ViewService( public ViewService(
IAccountRepository accountRepository, IAccountRepository accountRepository,
@ -26,16 +26,16 @@ namespace Schaad.Accounting.Services
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
ISubclassRepository subclassRepository, ISubclassRepository subclassRepository,
IBookingRuleRepository bookingRuleRepository, IBookingRuleRepository bookingRuleRepository,
IFxConverter fxConverter, IFxService fxService,
ILogger<ViewService> logger) ISettingsService settingsService)
{ {
this.accountRepository = accountRepository; this.accountRepository = accountRepository;
this.bankTransactionRepository = bankTransactionRepository; this.bankTransactionRepository = bankTransactionRepository;
this.transactionRepository = transactionRepository; this.transactionRepository = transactionRepository;
this.subclassRepository = subclassRepository; this.subclassRepository = subclassRepository;
this.bookingRuleRepository = bookingRuleRepository; this.bookingRuleRepository = bookingRuleRepository;
this.fxConverter = fxConverter; this.fxService = fxService;
this.logger = logger; this.settingsService = settingsService;
} }
/// <summary> /// <summary>
@ -55,6 +55,7 @@ namespace Schaad.Accounting.Services
var accounts = accountRepository.GetAccountList(); var accounts = accountRepository.GetAccountList();
var transactionList = GetTransactionViewList(); var transactionList = GetTransactionViewList();
var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name); var subClassNameByNumber = subclassRepository.GetSubClassList().ToDictionary(s => s.Number, s => s.Name);
var settings = settingsService.GetSettings();
// Group transactions by account so per-account balance is O(k) instead of O(M). // 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()); var creditsByAccount = transactionList.GroupBy(t => t.TargetAccountId).ToDictionary(g => g.Key, g => g.ToList());
@ -66,8 +67,8 @@ namespace Schaad.Accounting.Services
return new AccountDataset( return new AccountDataset(
account: a, account: a,
balance: balance, balance: balance,
balanceCHF: fxConverter.ConvertToChf(balance, a.Currency), balanceCHF: fxService.ConvertCurrency(balance, a.Currency, "CHF", settings.FixerIoApiKey),
startBalanceCHF: fxConverter.ConvertToChf(a.StartBalance, a.Currency), startBalanceCHF: fxService.ConvertCurrency(a.StartBalance, a.Currency, "CHF", settings.FixerIoApiKey),
className: subclassRepository.GetClass(a.Class), className: subclassRepository.GetClass(a.Class),
subClassName: subClassNameByNumber[a.SubClass]); subClassName: subClassNameByNumber[a.SubClass]);
}) })
@ -110,8 +111,9 @@ namespace Schaad.Accounting.Services
{ {
var accountList = GetAccountViewList(); var accountList = GetAccountViewList();
var settings = settingsService.GetSettings();
var profit = Math.Abs(accountList.Where(m => m.Class == ClassIds.Income).Sum(m =>m.BalanceCHF)); 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))); profit += Math.Abs(accountList.Where(m => m.Class == ClassIds.Activa).Sum(m => fxService.ConvertCurrency(m.StartBalance, m.Currency, "CHF", settings.FixerIoApiKey)));
var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF)); var loss = Math.Abs(accountList.Where(m => m.Class == ClassIds.Expenses).Sum(m => m.BalanceCHF));
var balanceView = new BalanceSheetDataset( var balanceView = new BalanceSheetDataset(
@ -163,7 +165,8 @@ namespace Schaad.Accounting.Services
var originAccount = accountsById[t.OriginAccountId]; var originAccount = accountsById[t.OriginAccountId];
if (originAccount.Class == ClassIds.Activa && accountId == t.OriginAccountId) if (originAccount.Class == ClassIds.Activa && accountId == t.OriginAccountId)
{ {
var copy = t.Clone(); var copy = new Transaction();
t.Copy(copy);
copy.Value *= -1; copy.Value *= -1;
return copy; return copy;
} }
@ -208,7 +211,7 @@ namespace Schaad.Accounting.Services
var transactions = transactionRepository.GetTransactionList(); var transactions = transactionRepository.GetTransactionList();
var accounts = accountRepository.GetAccountList(); var accounts = accountRepository.GetAccountList();
var bookingRules = bookingRuleRepository.GetBookingRuleList(); var bookingRules = bookingRuleRepository.GetBookingRuleList();
var bankTransactions = GetOpenBankTransactionList().OrderBy(t => t.ValueDate).ToList(); var bankTransactions = GetOpenBankTransactionList().OrderBy(t => t.ValueDate);
foreach (var bankTransaction in bankTransactions) foreach (var bankTransaction in bankTransactions)
{ {
var trx = new Transaction(bankTransaction, accounts); var trx = new Transaction(bankTransaction, accounts);
@ -217,9 +220,6 @@ namespace Schaad.Accounting.Services
MatchBankTransactionBySameValueLastMonth(bankTransaction, trx, transactions); MatchBankTransactionBySameValueLastMonth(bankTransaction, trx, transactions);
newTransactionList.Add(trx); newTransactionList.Add(trx);
} }
var matched = newTransactionList.Count(t => string.IsNullOrEmpty(t.Text) == false);
logger.LogInformation("Matched {Matched} of {Total} open bank transactions", matched, bankTransactions.Count);
return newTransactionList; return newTransactionList;
} }

View file

@ -1,4 +1,3 @@
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute; using NSubstitute;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -22,7 +21,7 @@ public class AccountRepositoryTestShould : IDisposable
settingsService.GetDbPath().Returns(dbDir); settingsService.GetDbPath().Returns(dbDir);
settingsService.GetLastYearDbPath().Returns(Path.Combine(dbDir, "no-such-dir")); settingsService.GetLastYearDbPath().Returns(Path.Combine(dbDir, "no-such-dir"));
sut = new AccountRepository(settingsService, new RepositoryCache(), NullLogger<AccountRepository>.Instance); sut = new AccountRepository(settingsService, new RepositoryCache());
} }
public void Dispose() public void Dispose()
@ -104,7 +103,7 @@ public class AccountRepositoryTestShould : IDisposable
sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" }); sut.SaveAccount(new Account { Number = 1000, Name = "Cash", Currency = "CHF" });
// Re-construct with the same directory: existing file, no year rollover, no data loss. // Re-construct with the same directory: existing file, no year rollover, no data loss.
var fresh = new AccountRepository(settingsService, new RepositoryCache(), NullLogger<AccountRepository>.Instance); var fresh = new AccountRepository(settingsService, new RepositoryCache());
var accounts = fresh.GetAccountList(); var accounts = fresh.GetAccountList();
accounts.Count.ShouldBe(1); accounts.Count.ShouldBe(1);

View file

@ -1,5 +1,4 @@
using System.Text; using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute; using NSubstitute;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -27,7 +26,7 @@ public class FileServiceTestShould
private FileService BuildService() => new( private FileService BuildService() => new(
settingsService, accountRepo, transactionRepo, bankTransactionRepo, settingsService, accountRepo, transactionRepo, bankTransactionRepo,
accountStatementService, creditCardStatementService, NullLogger<FileService>.Instance); accountStatementService, creditCardStatementService);
[Fact] [Fact]
public void EmitHeaderRowAndRunningBalanceWhenExportingTransactionsCsv() public void EmitHeaderRowAndRunningBalanceWhenExportingTransactionsCsv()

View file

@ -1,34 +0,0 @@
using NSubstitute;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Services;
using Schaad.Finance.Api;
using Shouldly;
namespace Schaad.Accounting.Tests;
public class FxConverterTestShould
{
private readonly IFxService fxService = Substitute.For<IFxService>();
private readonly SettingsDataset settings = new() { DataPath = "", FixerIoApiKey = "test-key" };
private FxConverter BuildConverter() => new(fxService, settings);
[Fact]
public void DelegateToFxServiceWithChfAsTargetAndConfiguredApiKeyWhenConverting()
{
fxService.ConvertCurrency(100m, "USD", "CHF", "test-key").Returns(90m);
var result = BuildConverter().ConvertToChf(100m, "USD");
result.ShouldBe(90m);
fxService.Received(1).ConvertCurrency(100m, "USD", "CHF", "test-key");
}
[Fact]
public void PassNegativeAmountsThroughWhenConverting()
{
fxService.ConvertCurrency(-50m, "EUR", "CHF", Arg.Any<string>()).Returns(-48m);
BuildConverter().ConvertToChf(-50m, "EUR").ShouldBe(-48m);
}
}

View file

@ -1,4 +1,3 @@
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute; using NSubstitute;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
@ -26,7 +25,7 @@ public class TransactionRepositoryTestShould : IDisposable
accountRepo = Substitute.For<IAccountRepository>(); accountRepo = Substitute.For<IAccountRepository>();
cache = new RepositoryCache(); cache = new RepositoryCache();
sut = new TransactionRepository(settingsService, cache, accountRepo, NullLogger<TransactionRepository>.Instance); sut = new TransactionRepository(settingsService, cache, accountRepo);
} }
public void Dispose() public void Dispose()
@ -136,43 +135,4 @@ public class TransactionRepositoryTestShould : IDisposable
sut.GetTransactionList().ShouldBeEmpty(); sut.GetTransactionList().ShouldBeEmpty();
} }
[Fact]
public void PreserveRelatedPartyWhenRoundTrippingTransaction()
{
accountRepo.GetAccount(Arg.Any<string>()).Returns(new Account { Currency = "CHF" });
sut.SaveTransaction(new Transaction
{
OriginAccountId = "a",
TargetAccountId = "b",
Value = 10m,
Text = "Rent",
RelatedParty = "ACME Property AG",
ValueDate = new DateTime(2026, 4, 1),
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");
}
[Fact]
public void DefaultBookingDateToValueDateWhenBookingDateIsUnset()
{
accountRepo.GetAccount(Arg.Any<string>()).Returns(new Account { Currency = "CHF" });
sut.SaveTransaction(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));
}
} }

View file

@ -1,8 +1,9 @@
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute; using NSubstitute;
using Schaad.Accounting.Datasets;
using Schaad.Accounting.Interfaces; using Schaad.Accounting.Interfaces;
using Schaad.Accounting.Models; using Schaad.Accounting.Models;
using Schaad.Accounting.Services; using Schaad.Accounting.Services;
using Schaad.Finance.Api;
using Shouldly; using Shouldly;
namespace Schaad.Accounting.Tests; namespace Schaad.Accounting.Tests;
@ -14,7 +15,8 @@ public class ViewServiceTestShould
private readonly IBankTransactionRepository bankTransactionRepo = Substitute.For<IBankTransactionRepository>(); private readonly IBankTransactionRepository bankTransactionRepo = Substitute.For<IBankTransactionRepository>();
private readonly IBookingRuleRepository bookingRuleRepo = Substitute.For<IBookingRuleRepository>(); private readonly IBookingRuleRepository bookingRuleRepo = Substitute.For<IBookingRuleRepository>();
private readonly ISubclassRepository subclassRepo = Substitute.For<ISubclassRepository>(); private readonly ISubclassRepository subclassRepo = Substitute.For<ISubclassRepository>();
private readonly IFxConverter fxConverter = Substitute.For<IFxConverter>(); private readonly IFxService fxService = Substitute.For<IFxService>();
private readonly ISettingsService settingsService = Substitute.For<ISettingsService>();
public ViewServiceTestShould() public ViewServiceTestShould()
{ {
@ -22,14 +24,15 @@ public class ViewServiceTestShould
subclassRepo.GetSubClassList().Returns( subclassRepo.GetSubClassList().Returns(
Enumerable.Range(10, 50).Select(n => new SubClass { Number = n, Name = "sub-" + n }).ToList()); Enumerable.Range(10, 50).Select(n => new SubClass { Number = n, Name = "sub-" + n }).ToList());
subclassRepo.GetClass(Arg.Any<int>()).Returns(""); subclassRepo.GetClass(Arg.Any<int>()).Returns("");
settingsService.GetSettings().Returns(new SettingsDataset { DataPath = "", FixerIoApiKey = "" });
// Passthrough FX by default (CHF-only). Individual tests can override. // Passthrough FX by default (CHF-only). Individual tests can override.
fxConverter.ConvertToChf(Arg.Any<decimal>(), Arg.Any<string>()) fxService.ConvertCurrency(Arg.Any<decimal>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(ci => ci.ArgAt<decimal>(0)); .Returns(ci => ci.ArgAt<decimal>(0));
} }
private ViewService BuildService() => private ViewService BuildService() =>
new(accountRepo, bankTransactionRepo, transactionRepo, subclassRepo, bookingRuleRepo, fxConverter, NullLogger<ViewService>.Instance); new(accountRepo, bankTransactionRepo, transactionRepo, subclassRepo, bookingRuleRepo, fxService, settingsService);
// --- Balance math --------------------------------------------------------- // --- Balance math ---------------------------------------------------------
@ -79,7 +82,7 @@ public class ViewServiceTestShould
transactionRepo.GetTransactionList().Returns(new List<Transaction>()); transactionRepo.GetTransactionList().Returns(new List<Transaction>());
// 1 USD = 0.90 CHF // 1 USD = 0.90 CHF
fxConverter.ConvertToChf(Arg.Any<decimal>(), "USD") fxService.ConvertCurrency(Arg.Any<decimal>(), "USD", "CHF", Arg.Any<string>())
.Returns(ci => ci.ArgAt<decimal>(0) * 0.9m); .Returns(ci => ci.ArgAt<decimal>(0) * 0.9m);
var account = BuildService().GetAccountViewList().Single(); var account = BuildService().GetAccountViewList().Single();

View file

@ -22,6 +22,9 @@ public partial class Home : ComponentBase
[Inject] [Inject]
private IDialogService dialogService { get; set; } = null!; private IDialogService dialogService { get; set; } = null!;
[Inject]
private ISettingsService settingsService { get; set; } = null!;
[Inject] [Inject]
private IFileService fileService { get; set; } = null!; private IFileService fileService { get; set; } = null!;
@ -44,11 +47,17 @@ public partial class Home : ComponentBase
bool IsCanceled; bool IsCanceled;
Dictionary<int, string> Files = new(); Dictionary<int, string> Files = new();
private bool loaded;
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
if (loaded == false) {
matchingBankTransactions = viewService.MatchOpenBankTransactions(); matchingBankTransactions = viewService.MatchOpenBankTransactions();
accounts = viewService.GetAccountViewList(); accounts = viewService.GetAccountViewList();
bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray(); bookingTexts = bookingTextRepository.GetBookingTextList().Select(b => b.Text).ToArray();
loaded = true;
}
return base.OnInitializedAsync(); return base.OnInitializedAsync();
} }

View file

@ -31,7 +31,6 @@ namespace Schaad.Accounting.UI
services.AddScoped<IAccountStatementService, AccountStatementService>(); services.AddScoped<IAccountStatementService, AccountStatementService>();
services.AddScoped<ICreditCardStatementService, CreditCardStatementService>(); services.AddScoped<ICreditCardStatementService, CreditCardStatementService>();
services.AddSingleton<IFxService, DummyFxService>(); services.AddSingleton<IFxService, DummyFxService>();
services.AddScoped<IFxConverter, FxConverter>();
services.AddSingleton<IPdfParsingService, PdfParsingService>(); services.AddSingleton<IPdfParsingService, PdfParsingService>();
return services; return services;

View file

@ -7,7 +7,6 @@
}, },
"Settings": { "Settings": {
"DataPath": "D:\\Developer\\AccountingData\\", "DataPath": "D:\\Developer\\AccountingData\\",
"FixerIoApiKey": "ee23142f1120bb1223b0e76f98abfa97", "FixerIoApiKey": "ee23142f1120bb1223b0e76f98abfa97"
"DefaultMandator": "Claudio Schaad"
} }
} }