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 void PreserveValueWhenRoundTrippingNonFxTransaction() { accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); accountRepo.GetAccount("groc").Returns(new Account { Id = "groc", Currency = "CHF" }); sut.SaveTransaction(new Transaction { OriginAccountId = "chk", TargetAccountId = "groc", Value = 42m, Text = "Groceries", ValueDate = new DateTime(2026, 5, 10), BookingDate = new DateTime(2026, 5, 10) }); var stored = sut.GetTransactionList().Single(); var loaded = sut.GetTransaction(stored.Id); loaded.ShouldNotBeNull(); loaded.Value.ShouldBe(42m); loaded.FxRate.ShouldBeNull(); // Non-FX transactions have FxRate cleared } [Fact] public void 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" }); sut.SaveTransaction(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 = sut.GetTransactionList().Single(); stored.Value.ShouldBe(90m); // stored in CHF var loaded = sut.GetTransaction(stored.Id); loaded.ShouldNotBeNull(); loaded.Value.ShouldBe(100m); // display currency loaded.FxRate.ShouldBe(0.9m); } [Fact] public void ReturnNullWhenGettingUnknownTransactionId() { sut.GetTransaction("no-such-id").ShouldBeNull(); } [Fact] public void NotMutateStoredValueWhenGettingFxTransactionTwice() { accountRepo.GetAccount("chk").Returns(new Account { Id = "chk", Currency = "CHF" }); accountRepo.GetAccount("usd").Returns(new Account { Id = "usd", Currency = "USD" }); sut.SaveTransaction(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 = sut.GetTransactionList().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); first.ShouldNotBeNull(); first.Value.ShouldBe(100m); second.ShouldNotBeNull(); second.Value.ShouldBe(100m); } [Fact] public void RemoveTransactionWhenDeletingById() { accountRepo.GetAccount(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; sut.DeleteTransaction(id); sut.GetTransactionList().ShouldBeEmpty(); } [Fact] public void PreserveRelatedPartyWhenRoundTrippingTransaction() { accountRepo.GetAccount(Arg.Any()).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()).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)); } }