using AutoFixture;
using AutoFixture.AutoMoq;
using AwesomeAssertions;
using Books.Api.EventFlow.ReadModels;
using Books.Api.EventFlow.Repositories;
using Books.Api.Reporting;
using Ledger.Core.Models;
using Ledger.Core.Services;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
namespace Books.Api.Tests.Reporting;
///
/// Unit tests for VatReportService.
/// Tests VAT report generation from ledger data for SKAT compliance.
///
[Trait("Category", "Unit")]
public class VatReportServiceTests
{
private readonly IFixture _fixture;
private readonly Mock _accountRepository;
private readonly Mock _ledgerService;
private readonly VatReportService _sut;
private const string CompanyId = "company-123";
private const string InputVatAccountNumber = "5610";
private const string OutputVatAccountNumber = "5611";
public VatReportServiceTests()
{
_fixture = new Fixture().Customize(new AutoMoqCustomization());
_accountRepository = new Mock();
_ledgerService = new Mock();
_sut = new VatReportService(
_accountRepository.Object,
_ledgerService.Object,
NullLogger.Instance);
}
#region Period Validation Tests
[Fact]
public async Task GenerateReportAsync_PeriodEndBeforeStart_ThrowsArgumentException()
{
// Arrange
var periodStart = new DateOnly(2024, 3, 1);
var periodEnd = new DateOnly(2024, 2, 1);
// Act
var act = () => _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
await act.Should().ThrowAsync()
.WithMessage("*startdato*");
}
[Fact]
public async Task GenerateReportAsync_PeriodExceedsOneYear_ThrowsArgumentException()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2025, 2, 1); // 397 days
// Act
var act = () => _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
await act.Should().ThrowAsync()
.WithMessage("*366*");
}
[Fact]
public async Task GenerateReportAsync_ValidPeriod_DoesNotThrow()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 12, 31); // 365 days - valid
SetupNoVatAccounts();
SetupEmptyLedgerResponse();
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert - should not throw, returns empty report
result.Should().NotBeNull();
}
#endregion
#region No VAT Accounts Tests
[Fact]
public async Task GenerateReportAsync_NoVatAccounts_ReturnsEmptyReport()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
SetupNoVatAccounts();
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(0);
result.BoxB.Should().Be(0);
result.BoxC.Should().Be(0);
result.BoxD.Should().Be(0);
result.NetVat.Should().Be(0);
result.TransactionCount.Should().Be(0);
result.PeriodStart.Should().Be(periodStart);
result.PeriodEnd.Should().Be(periodEnd);
}
#endregion
#region Output VAT Tests
[Fact]
public async Task GenerateReportAsync_OnlyOutputVat_ReturnsCorrectBoxA()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
var outputGuid = Guid.NewGuid();
SetupInputVatAccount(null);
SetupOutputVatAccount(outputGuid, "Salgsmoms");
SetupLedgerResponse(new AccountPeriodBalance
{
AccountId = outputGuid,
TotalCredits = 25000m,
TotalDebits = 0m,
NetChange = -25000m,
EntryCount = 50,
Currency = "DKK"
});
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(25000m);
result.BoxB.Should().Be(0);
result.TotalOutputVat.Should().Be(25000m);
result.TotalInputVat.Should().Be(0);
result.NetVat.Should().Be(25000m);
result.TransactionCount.Should().Be(50);
}
#endregion
#region Input VAT Tests
[Fact]
public async Task GenerateReportAsync_OnlyInputVat_ReturnsCorrectBoxB()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
var inputGuid = Guid.NewGuid();
SetupInputVatAccount(inputGuid, "Købsmoms");
SetupOutputVatAccount(null);
SetupLedgerResponse(new AccountPeriodBalance
{
AccountId = inputGuid,
TotalDebits = 15000m,
TotalCredits = 0m,
NetChange = 15000m,
EntryCount = 30,
Currency = "DKK"
});
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(0);
result.BoxB.Should().Be(15000m);
result.TotalOutputVat.Should().Be(0);
result.TotalInputVat.Should().Be(15000m);
result.NetVat.Should().Be(-15000m); // Negative = refund
result.TransactionCount.Should().Be(30);
}
#endregion
#region Mixed VAT Tests
[Fact]
public async Task GenerateReportAsync_BothInputAndOutputVat_CalculatesNetCorrectly()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
var inputGuid = Guid.NewGuid();
var outputGuid = Guid.NewGuid();
SetupInputVatAccount(inputGuid, "Købsmoms");
SetupOutputVatAccount(outputGuid, "Salgsmoms");
SetupLedgerResponse(
new AccountPeriodBalance
{
AccountId = inputGuid,
TotalDebits = 10000m,
TotalCredits = 0m,
NetChange = 10000m,
EntryCount = 20,
Currency = "DKK"
},
new AccountPeriodBalance
{
AccountId = outputGuid,
TotalCredits = 25000m,
TotalDebits = 0m,
NetChange = -25000m,
EntryCount = 40,
Currency = "DKK"
});
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(25000m);
result.BoxB.Should().Be(10000m);
result.TotalOutputVat.Should().Be(25000m);
result.TotalInputVat.Should().Be(10000m);
result.NetVat.Should().Be(15000m); // 25000 - 10000 = to pay
result.TransactionCount.Should().Be(60);
}
#endregion
#region Basis Calculation Tests
[Fact]
public async Task GenerateReportAsync_WithOutputVat_CalculatesBasis1From25PercentRate()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
var outputGuid = Guid.NewGuid();
SetupInputVatAccount(null);
SetupOutputVatAccount(outputGuid, "Salgsmoms");
SetupLedgerResponse(new AccountPeriodBalance
{
AccountId = outputGuid,
TotalCredits = 2500m, // 25% VAT on 10000
TotalDebits = 0m,
NetChange = -2500m,
EntryCount = 10,
Currency = "DKK"
});
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(2500m);
result.Basis1.Should().Be(10000m); // 2500 / 0.25 = 10000
}
#endregion
#region Empty Period Tests
[Fact]
public async Task GenerateReportAsync_EmptyPeriod_ReturnsZeroAmounts()
{
// Arrange
var periodStart = new DateOnly(2024, 1, 1);
var periodEnd = new DateOnly(2024, 3, 31);
var inputGuid = Guid.NewGuid();
var outputGuid = Guid.NewGuid();
SetupInputVatAccount(inputGuid, "Købsmoms");
SetupOutputVatAccount(outputGuid, "Salgsmoms");
SetupEmptyLedgerResponse();
// Act
var result = await _sut.GenerateReportAsync(CompanyId, periodStart, periodEnd);
// Assert
result.BoxA.Should().Be(0);
result.BoxB.Should().Be(0);
result.TransactionCount.Should().Be(0);
result.NetVat.Should().Be(0);
}
#endregion
#region Helper Methods
private void SetupNoVatAccounts()
{
_accountRepository
.Setup(x => x.GetByCompanyAndNumberAsync(
It.IsAny(),
It.IsAny(),
It.IsAny()))
.ReturnsAsync((AccountReadModelDto?)null);
}
private void SetupEmptyLedgerResponse()
{
_ledgerService
.Setup(x => x.QueryEntriesAsync(
It.IsAny(),
It.IsAny()))
.ReturnsAsync(new EntriesQueryResult { Aggregates = [] });
}
private void SetupLedgerResponse(params AccountPeriodBalance[] balances)
{
_ledgerService
.Setup(x => x.QueryEntriesAsync(
It.IsAny(),
It.IsAny()))
.ReturnsAsync(new EntriesQueryResult { Aggregates = balances.ToList() });
}
private void SetupInputVatAccount(Guid? accountGuid, string name = "Købsmoms")
{
var account = accountGuid.HasValue
? new AccountReadModelDto
{
Id = $"account-{accountGuid.Value}",
CompanyId = CompanyId,
AccountNumber = InputVatAccountNumber,
Name = name
}
: null;
_accountRepository
.Setup(x => x.GetByCompanyAndNumberAsync(
CompanyId,
InputVatAccountNumber,
It.IsAny()))
.ReturnsAsync(account);
}
private void SetupOutputVatAccount(Guid? accountGuid, string name = "Salgsmoms")
{
var account = accountGuid.HasValue
? new AccountReadModelDto
{
Id = $"account-{accountGuid.Value}",
CompanyId = CompanyId,
AccountNumber = OutputVatAccountNumber,
Name = name
}
: null;
_accountRepository
.Setup(x => x.GetByCompanyAndNumberAsync(
CompanyId,
OutputVatAccountNumber,
It.IsAny()))
.ReturnsAsync(account);
}
#endregion
}