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; }
}
}