using Books.Api.AiBookkeeper; using Books.Api.EventFlow.ReadModels; using Books.Api.EventFlow.Repositories; using Moq; using AwesomeAssertions; namespace Books.Api.Tests.AiBookkeeper; /// /// Unit tests for ChartOfAccountsProvider. /// Verifies that accounts are filtered and sorted correctly. /// [Trait("Category", "Unit")] public class ChartOfAccountsProviderTests { private readonly Mock _accountRepository; private readonly ChartOfAccountsProvider _sut; public ChartOfAccountsProviderTests() { _accountRepository = new Mock(); _sut = new ChartOfAccountsProvider(_accountRepository.Object); } [Fact] public async Task GetChartOfAccountsAsync_ReturnsOnlyExpenseTypeAccounts() { // Arrange var accounts = new List { CreateAccount("1000", "Bank", "asset", null), CreateAccount("2000", "Leverandørgæld", "liability", null), CreateAccount("3000", "Egenkapital", "equity", null), CreateAccount("4000", "Omsætning", "income", null), CreateAccount("5000", "Vareforbrug", "cogs", "I25"), CreateAccount("6000", "Løn", "personnel", "INGEN"), CreateAccount("7000", "Kontorudgifter", "expense", "I25"), CreateAccount("8000", "Renteudgifter", "financial", null) }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert - Only expense-type accounts should be included result.Accounts.Should().HaveCount(4); result.Accounts.Select(a => a.AccountType).Should().OnlyContain(t => t == "cogs" || t == "personnel" || t == "expense" || t == "financial"); } [Fact] public async Task GetChartOfAccountsAsync_ExcludesAssetAccounts() { // Arrange var accounts = new List { CreateAccount("1000", "Bank", "asset", null), CreateAccount("1200", "Varelager", "asset", null), CreateAccount("7000", "Kontorudgifter", "expense", "I25") }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert result.Accounts.Should().HaveCount(1); result.Accounts.Should().NotContain(a => a.AccountType == "asset"); } [Fact] public async Task GetChartOfAccountsAsync_ExcludesLiabilityAccounts() { // Arrange var accounts = new List { CreateAccount("2000", "Leverandørgæld", "liability", null), CreateAccount("2100", "Skyldig moms", "liability", null), CreateAccount("7000", "Kontorudgifter", "expense", "I25") }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert result.Accounts.Should().HaveCount(1); result.Accounts.Should().NotContain(a => a.AccountType == "liability"); } [Fact] public async Task GetChartOfAccountsAsync_ExcludesIncomeAccounts() { // Arrange var accounts = new List { CreateAccount("4000", "Omsætning DK", "income", "U25"), CreateAccount("4100", "Omsætning EU", "income", "U25EU"), CreateAccount("7000", "Kontorudgifter", "expense", "I25") }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert result.Accounts.Should().HaveCount(1); result.Accounts.Should().NotContain(a => a.AccountType == "income"); } [Fact] public async Task GetChartOfAccountsAsync_SortsAccountsByNumber() { // Arrange var accounts = new List { CreateAccount("7200", "IT-udgifter", "expense", "I25"), CreateAccount("5000", "Vareforbrug", "cogs", "I25"), CreateAccount("7100", "Kontorudgifter", "expense", "I25"), CreateAccount("6000", "Løn", "personnel", null) }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert var accountNumbers = result.Accounts.Select(a => a.AccountNumber).ToList(); accountNumbers.Should().BeInAscendingOrder(); accountNumbers.Should().Equal(["5000", "6000", "7100", "7200"]); } [Fact] public async Task GetChartOfAccountsAsync_ReturnsCorrectCompanyId() { // Arrange _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync("company-42", It.IsAny())) .ReturnsAsync([]); // Act var result = await _sut.GetChartOfAccountsAsync("company-42"); // Assert result.CompanyId.Should().Be("company-42"); } [Fact] public async Task GetChartOfAccountsAsync_MapsAllAccountProperties() { // Arrange var accounts = new List { new() { Id = "acc-1", CompanyId = "company-1", AccountNumber = "7320", Name = "Software abonnementer", AccountType = "expense", VatCodeId = "I25", StandardAccountNumber = "11400", IsActive = true, IsSystemAccount = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow } }; _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(accounts); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert result.Accounts.Should().HaveCount(1); var account = result.Accounts[0]; account.AccountNumber.Should().Be("7320"); account.Name.Should().Be("Software abonnementer"); account.AccountType.Should().Be("expense"); account.VatCodeId.Should().Be("I25"); account.StandardAccountNumber.Should().Be("11400"); } [Fact] public async Task GetChartOfAccountsAsync_HandlesEmptyAccountList() { // Arrange _accountRepository.Setup(r => r.GetActiveByCompanyIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync([]); // Act var result = await _sut.GetChartOfAccountsAsync("company-1"); // Assert result.Accounts.Should().BeEmpty(); result.CompanyId.Should().Be("company-1"); } private static AccountReadModelDto CreateAccount(string number, string name, string type, string? vatCode) { return new AccountReadModelDto { Id = $"account-{number}", CompanyId = "company-1", AccountNumber = number, Name = name, AccountType = type, VatCodeId = vatCode, IsActive = true, IsSystemAccount = false, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; } }