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 }