using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; using Schaad.Accounting.Repositories; using Shouldly; namespace Schaad.Accounting.Tests; public class TransactionRepositoryTestShould : IDisposable { private readonly string dbDir; private readonly ISettingsService settingsService; private readonly IAccountRepository accountRepo; private readonly RepositoryCache cache; private readonly TransactionRepository sut; public TransactionRepositoryTestShould() { dbDir = Path.Combine(Path.GetTempPath(), "AccountingNext-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(dbDir); settingsService = Substitute.For(); settingsService.GetDbPath().Returns(dbDir); settingsService.GetLastYearDbPath().Returns(Path.Combine(dbDir, "no-such-dir")); accountRepo = Substitute.For(); cache = new RepositoryCache(); sut = new TransactionRepository(settingsService, cache, accountRepo, NullLogger.Instance); } public void Dispose() { if (Directory.Exists(dbDir)) { Directory.Delete(dbDir, recursive: true); } } [Fact] public async Task PreserveValueWhenRoundTrippingNonFxTransaction() { accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); accountRepo.GetAccountAsync("groc").Returns(new Account { Id = "groc", Currency = "CHF" }); await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "groc", Value = 42m, Text = "Groceries", ValueDate = new DateTime(2026, 5, 10), BookingDate = new DateTime(2026, 5, 10) }); var stored = (await sut.GetTransactionListAsync()).Single(); var loaded = await sut.GetTransactionAsync(stored.Id); loaded.ShouldNotBeNull(); loaded.Value.ShouldBe(42m); loaded.FxRate.ShouldBeNull(); } [Fact] public async Task StoreInChfAndReturnInAccountCurrencyWhenTransactionIsFx() { accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); accountRepo.GetAccountAsync("usd").Returns(new Account { Id = "usd", Currency = "USD" }); await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "usd", Value = 100m, FxRate = 0.9m, Text = "USD transfer", ValueDate = new DateTime(2026, 5, 10), BookingDate = new DateTime(2026, 5, 10) }); var stored = (await sut.GetTransactionListAsync()).Single(); stored.Value.ShouldBe(90m); var loaded = await sut.GetTransactionAsync(stored.Id); loaded.ShouldNotBeNull(); loaded.Value.ShouldBe(100m); loaded.FxRate.ShouldBe(0.9m); } [Fact] public async Task ReturnNullWhenGettingUnknownTransactionId() { (await sut.GetTransactionAsync("no-such-id")).ShouldBeNull(); } [Fact] public async Task NotMutateStoredValueWhenGettingFxTransactionTwice() { accountRepo.GetAccountAsync("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); accountRepo.GetAccountAsync("usd").Returns(new Account { Id = "usd", Currency = "USD" }); await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "chk", TargetAccountId = "usd", Value = 100m, FxRate = 0.9m, Text = "USD transfer", ValueDate = new DateTime(2026, 5, 10), BookingDate = new DateTime(2026, 5, 10) }); var stored = (await sut.GetTransactionListAsync()).Single(); var id = stored.Id; var first = await sut.GetTransactionAsync(id); var second = await sut.GetTransactionAsync(id); first.ShouldNotBeNull(); first.Value.ShouldBe(100m); second.ShouldNotBeNull(); second.Value.ShouldBe(100m); } [Fact] public async Task RemoveTransactionWhenDeletingById() { accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 1m, Text = "x", ValueDate = DateTime.Today }); var id = (await sut.GetTransactionListAsync()).Single().Id; await sut.DeleteTransactionAsync(id); (await sut.GetTransactionListAsync()).ShouldBeEmpty(); } [Fact] public async Task PreserveRelatedPartyWhenRoundTrippingTransaction() { accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); await sut.SaveTransactionAsync(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) }); (await sut.GetTransactionListAsync()).Single().RelatedParty.ShouldBe("ACME Property AG"); } [Fact] public async Task DefaultBookingDateToValueDateWhenBookingDateIsUnset() { accountRepo.GetAccountAsync(Arg.Any()).Returns(new Account { Currency = "CHF" }); await sut.SaveTransactionAsync(new Transaction { OriginAccountId = "a", TargetAccountId = "b", Value = 10m, Text = "x", ValueDate = new DateTime(2026, 4, 15) }); (await sut.GetTransactionListAsync()).Single().BookingDate.ShouldBe(new DateTime(2026, 4, 15)); } }