AccountingNext/Schaad.Accounting.Tests/FileServiceTestShould.cs
Claudio Schaad 7287dbd440 PR H: log bank-statement imports via ILogger<FileService>
FileService.ImportAccountStatementFile now emits Information on the
file being processed and on each account's import count, Warning on
mandator-mismatch skips, and Error on vendor-parser failures. UI-facing
MessageDataset behaviour is unchanged.

Pulls Microsoft.Extensions.Logging.Abstractions into the Services
project; FileServiceTestShould uses NullLogger<FileService>.Instance.

Rest of item 18 (repository logging, MatchOpenBankTransactions
summary) tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 21:30:41 +02:00

107 lines
5.2 KiB
C#

using System.Text;
using Microsoft.Extensions.Logging.Abstractions;
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<ISettingsService>();
private readonly IAccountRepository accountRepo = Substitute.For<IAccountRepository>();
private readonly ITransactionRepository transactionRepo = Substitute.For<ITransactionRepository>();
private readonly IBankTransactionRepository bankTransactionRepo = Substitute.For<IBankTransactionRepository>();
private readonly IAccountStatementService accountStatementService = Substitute.For<IAccountStatementService>();
private readonly ICreditCardStatementService creditCardStatementService = Substitute.For<ICreditCardStatementService>();
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, NullLogger<FileService>.Instance);
[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<Transaction>
{
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<Transaction>
{
// 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<Transaction> { outgoing });
BuildService().GetTransactionListCsv("chk");
// Regression: earlier code did `trx.Value *= -1` mid-loop.
outgoing.Value.ShouldBe(42m);
}
}