using Books.Api.EventFlow.Repositories; using Books.Api.EventFlow.Subscribers; using Books.Api.Tests.Helpers; using Books.Api.Tests.Infrastructure; using AwesomeAssertions; using Microsoft.Extensions.DependencyInjection; namespace Books.Api.Tests.GraphQL; /// /// Integration tests for ChartOfAccountsInitializer. /// Verifies that creating a company automatically bootstraps the standard Danish chart of accounts. /// [Trait("Category", "Integration")] public class ChartOfAccountsInitializerTests(TestWebApplicationFactory factory) : IntegrationTestBase(factory) { [Fact] public async Task CreateCompany_AutomaticallyCreatesStandardAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); var expectedAccountCount = StandardDanishAccounts.GetAll().Count(); // Act - Create a company var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id name } } """, new { input = new { name = "Auto-Account Test Virksomhed" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Wait for accounts to be created (eventually consistent) var accounts = await Eventually.GetListAsync( async () => { var repo = GetService(); return await repo.GetByCompanyIdAsync(companyId); }, expectedAccountCount, timeout: TimeSpan.FromSeconds(30), // Allow more time for many accounts failMessage: $"Expected {expectedAccountCount} accounts to be created"); accounts.Should().HaveCount(expectedAccountCount); } [Fact] public async Task CreateCompany_CreatesRevenueAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Revenue Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check revenue accounts (1xxx) var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var revenue = all.Where(a => a.AccountType == "revenue").ToList(); return revenue.Count >= 5 ? revenue : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().NotBeEmpty(); accounts.Should().Contain(a => a.AccountNumber == "1000"); // Salg af varer, DK accounts.Should().Contain(a => a.AccountNumber == "1200"); // Salg af ydelser, DK accounts.Should().OnlyContain(a => a.AccountType == "revenue"); } [Fact] public async Task CreateCompany_CreatesExpenseAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Expense Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var expenses = all.Where(a => a.AccountType == "expense").ToList(); return expenses.Count >= 10 ? expenses : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().NotBeEmpty(); accounts.Should().Contain(a => a.AccountNumber == "4000"); // Annoncer og reklame accounts.Should().Contain(a => a.AccountNumber == "5000"); // Husleje accounts.Should().Contain(a => a.AccountNumber == "7320"); // Køb af software } [Fact] public async Task CreateCompany_CreatesSystemAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "System Accounts Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check system accounts exist (we have many: equity, revenue, debtors, creditors, tax) var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var system = all.Where(a => a.IsSystemAccount).ToList(); return system.Count >= 15 ? system : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().NotBeEmpty(); accounts.Should().Contain(a => a.AccountNumber == "3000"); // Aktiekapital accounts.Should().Contain(a => a.AccountNumber == "3900"); // Overført resultat accounts.Should().Contain(a => a.AccountNumber == "3910"); // Årets resultat accounts.Should().Contain(a => a.AccountNumber == "1900"); // Debitorer accounts.Should().Contain(a => a.AccountNumber == "6900"); // Kreditorer accounts.Should().Contain(a => a.AccountNumber == "7950"); // Skyldig selskabsskat accounts.Should().OnlyContain(a => a.IsSystemAccount); } [Fact] public async Task CreateCompany_CreatesLiabilityAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Liability Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var liabilities = all.Where(a => a.AccountType == "liability").ToList(); return liabilities.Count >= 5 ? liabilities : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().NotBeEmpty(); accounts.Should().Contain(a => a.AccountNumber == "6900"); // Kreditorer accounts.Should().Contain(a => a.AccountNumber == "7900"); // Skyldig moms accounts.Should().Contain(a => a.AccountNumber == "7910"); // Skyldig A-skat } [Fact] public async Task CreateCompany_AccountsHaveCorrectVatCodes() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "VAT Code Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check that accounts have correct VAT codes var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); return all.Count >= 50 ? all.ToList() : null; }, timeout: TimeSpan.FromSeconds(30)); // Vareforbrug should have I25 (25% input VAT) var vareforbrug = accounts!.FirstOrDefault(a => a.AccountNumber == "2000"); vareforbrug.Should().NotBeNull(); vareforbrug!.VatCodeId.Should().Be("I25"); // Salg af varer DK should have U25 (25% output VAT) var salgVarer = accounts.FirstOrDefault(a => a.AccountNumber == "1000"); salgVarer.Should().NotBeNull(); salgVarer!.VatCodeId.Should().Be("U25"); // Bankrenter should have no VAT var bankrenter = accounts.FirstOrDefault(a => a.AccountNumber == "9200"); bankrenter.Should().NotBeNull(); bankrenter!.VatCodeId.Should().BeNull(); } [Fact] public async Task Query_AccountsViaGraphQL_ReturnsBootstrappedAccounts() { // Arrange var graphqlClient = new GraphQLTestClient(Client); // Create company var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "GraphQL Query Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Set company ID header for subsequent requests graphqlClient.SetCompanyId(companyId); // Wait for accounts to be created await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); return all.Count >= 50 ? all : null; }, timeout: TimeSpan.FromSeconds(30)); // Act - Query via GraphQL var queryResponse = await graphqlClient.QueryAsync(""" query Accounts($companyId: ID!) { accounts(companyId: $companyId) { id accountNumber name accountType isSystemAccount } } """, new { companyId }); // Assert queryResponse.EnsureNoErrors(); queryResponse.Data!.Accounts.Should().NotBeEmpty(); queryResponse.Data.Accounts.Should().Contain(a => a.AccountNumber == "2000"); queryResponse.Data.Accounts.Should().Contain(a => a.Name == "Vareforbrug"); } [Fact] public async Task ChartOfAccountsInitializer_IsIdempotent_CalledTwice() { // Arrange var graphqlClient = new GraphQLTestClient(Client); var expectedAccountCount = StandardDanishAccounts.GetAll().Count(); // Create company (which triggers first initialization) var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Idempotency Test Company" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Wait for first initialization to complete var initialAccounts = await Eventually.GetListAsync( async () => { var repo = GetService(); return await repo.GetByCompanyIdAsync(companyId); }, expectedAccountCount, timeout: TimeSpan.FromSeconds(30)); initialAccounts.Should().HaveCount(expectedAccountCount); // Act - Call initializer again (should be idempotent) var initializer = GetService(); await initializer.InitializeAsync(companyId); // Assert - Should still have the same number of accounts (not doubled) var accountsAfterSecondInit = await Eventually.GetAsync(async () => { var repo = GetService(); return await repo.GetByCompanyIdAsync(companyId); }); accountsAfterSecondInit.Should().HaveCount(expectedAccountCount, "Calling initializer twice should not create duplicate accounts"); } #region Danish Compliance: Chart of Accounts Structure Tests [Fact] public async Task ChartOfAccounts_ContainsShareCapitalAccount() { // Arrange - Danish companies are required to have an equity account for share capital var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Share Capital Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check that Aktiekapital (3000) exists var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); return all.Any(a => a.AccountNumber == "3000") ? all.ToList() : null; }, timeout: TimeSpan.FromSeconds(30)); var shareCapital = accounts!.FirstOrDefault(a => a.AccountNumber == "3000"); shareCapital.Should().NotBeNull("Aktiekapital/Anpartskapital account (3000) is required per Selskabsloven"); shareCapital!.AccountType.Should().Be("equity"); shareCapital.IsSystemAccount.Should().BeTrue(); } [Fact] public async Task ChartOfAccounts_ContainsCorporateTaxAccount() { // Arrange - Danish companies need a liability account for corporate tax var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Corporate Tax Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check that Skyldig selskabsskat (7950) exists var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); return all.Any(a => a.AccountNumber == "7950") ? all.ToList() : null; }, timeout: TimeSpan.FromSeconds(30)); var corporateTax = accounts!.FirstOrDefault(a => a.AccountNumber == "7950"); corporateTax.Should().NotBeNull("Skyldig selskabsskat account (7950) is required per Selskabsloven"); corporateTax!.AccountType.Should().Be("liability"); corporateTax.IsSystemAccount.Should().BeTrue(); } [Fact] public async Task ChartOfAccounts_HasProperEquityRange() { // Arrange - Per Danish accounting standards, accounts 3000-3999 should only be Equity var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Equity Range Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check that all 3xxx accounts are Equity type var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var equityRange = all.Where(a => int.TryParse(a.AccountNumber, out var num) && num >= 3000 && num < 4000).ToList(); return equityRange.Count >= 3 ? equityRange : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().NotBeEmpty("3xxx range should have equity accounts"); accounts.Should().OnlyContain( a => a.AccountType == "equity", "Per Danish accounting standards, 3xxx range should ONLY contain equity accounts"); } [Fact] public async Task ChartOfAccounts_ContainsFixedAssetAccounts() { // Arrange - Danish chart of accounts should include fixed asset accounts for depreciation var graphqlClient = new GraphQLTestClient(Client); // Act var createResponse = await graphqlClient.MutateAsync(""" mutation CreateCompany($input: CreateCompanyInput!) { createCompany(input: $input) { id } } """, new { input = new { name = "Fixed Assets Test" } }); createResponse.EnsureNoErrors(); var companyId = createResponse.Data!.CreateCompany!.Id; // Assert - Check that fixed asset accounts exist (1710-1730) var accounts = await Eventually.GetAsync(async () => { var repo = GetService(); var all = await repo.GetByCompanyIdAsync(companyId); var fixedAssets = all.Where(a => new[] { "1710", "1720", "1730" }.Contains(a.AccountNumber)).ToList(); return fixedAssets.Count >= 3 ? all.ToList() : null; }, timeout: TimeSpan.FromSeconds(30)); accounts.Should().Contain(a => a.AccountNumber == "1710", "Bygninger account required"); accounts.Should().Contain(a => a.AccountNumber == "1720", "Maskiner og inventar account required"); accounts.Should().Contain(a => a.AccountNumber == "1730", "Køretøjer account required"); // Also verify corresponding depreciation accounts exist accounts.Should().Contain(a => a.AccountNumber == "8010", "Afskrivning, bygninger account required"); accounts.Should().Contain(a => a.AccountNumber == "8020", "Afskrivning, maskiner account required"); accounts.Should().Contain(a => a.AccountNumber == "8030", "Afskrivning, køretøjer account required"); } #endregion // Response DTOs private class CreateCompanyResponse { public CompanyDto? CreateCompany { get; set; } } private class AccountsResponse { public List Accounts { get; set; } = []; } private class CompanyDto { public string Id { get; set; } = string.Empty; } private class AccountDto { public string Id { get; set; } = string.Empty; public string AccountNumber { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public string AccountType { get; set; } = string.Empty; public string? VatCodeId { get; set; } public bool IsSystemAccount { get; set; } } }