using System.Text; using NSubstitute; using Schaad.Accounting.Interfaces; using Schaad.Accounting.Models; using Schaad.Accounting.Services; using Schaad.Finance.Api; using Shouldly; namespace Schaad.Accounting.Tests; public class FileServiceTestShould { private readonly ISettingsService settingsService = Substitute.For(); private readonly IAccountRepository accountRepo = Substitute.For(); private readonly ITransactionRepository transactionRepo = Substitute.For(); private readonly IBankTransactionRepository bankTransactionRepo = Substitute.For(); private readonly IAccountStatementService accountStatementService = Substitute.For(); private readonly ICreditCardStatementService creditCardStatementService = Substitute.For(); 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); } private FileService BuildService() => new( settingsService, accountRepo, transactionRepo, bankTransactionRepo, accountStatementService, creditCardStatementService); [Fact] public void 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 { new() { Id = "t1", OriginAccountId = "chk", TargetAccountId = "groc", Value = 50m, Text = "Coop", BookingDate = new DateTime(2026, 1, 5), ValueDate = new DateTime(2026, 1, 5) }, new() { Id = "t2", OriginAccountId = "sal", TargetAccountId = "chk", Value = 5000m, Text = "Salary", BookingDate = new DateTime(2026, 1, 25), ValueDate = new DateTime(2026, 1, 25) } }); var bytes = BuildService().GetTransactionListCsv("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() { var checking = new Account { Id = "chk", Number = 1010, Name = "Checking", Currency = "CHF", StartBalance = 0m }; accountRepo.GetAccount("chk").Returns(checking); transactionRepo.GetTransactionList().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", BookingDate = new DateTime(2026, 1, 15), ValueDate = new DateTime(2026, 1, 15) }, new() { OriginAccountId = "chk", TargetAccountId = "x", Value = 20m, Text = "earlier large", BookingDate = new DateTime(2026, 1, 15), ValueDate = new DateTime(2026, 1, 15) } }); var csv = Encoding.GetEncoding("ISO-8859-1").GetString(BuildService().GetTransactionListCsv("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() { var checking = new Account { Id = "chk", Number = 1010, Name = "Checking", Currency = "CHF" }; accountRepo.GetAccount("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 }); BuildService().GetTransactionListCsv("chk"); // Regression: earlier code did `trx.Value *= -1` mid-loop. outgoing.Value.ShouldBe(42m); } }