using Books.Api.Domain;
using Books.Api.Domain.JournalEntryDrafts;
using AwesomeAssertions;
namespace Books.Api.Tests.Domain;
///
/// Unit tests for VatCalculationService.
/// Tests Danish VAT calculation logic for SKAT compliance.
///
[Trait("Category", "Unit")]
public class VatCalculationServiceTests
{
private readonly VatCalculationService _sut = new();
private const string InputVatAccount = "5610"; // Købsmoms (indgående moms)
private const string OutputVatAccount = "5611"; // Salgsmoms (udgående moms)
#region Sales (U25) Tests
[Fact]
public void CalculateVat_SalesWithU25_Exclusive_CalculatesCorrectVat()
{
// Arrange - Sale of 1000 kr excl. VAT
var lines = new List
{
new(1, "1000", 0m, 1000m, "Salg af varer", "U25") // Credit = sales revenue
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
result.VatLines.Should().HaveCount(1);
var vatLine = result.VatLines[0];
vatLine.CreditAmount.Should().Be(250m); // 25% of 1000
vatLine.DebitAmount.Should().Be(0m);
vatLine.AccountId.Should().Be(OutputVatAccount);
vatLine.VatCode.Should().Be("U25");
result.VatSummary.Should().HaveCount(1);
result.VatSummary[0].BaseAmount.Should().Be(1000m);
result.VatSummary[0].VatAmount.Should().Be(250m);
result.VatSummary[0].IsInputVat.Should().BeFalse();
}
[Fact]
public void CalculateVat_SalesWithU25_Inclusive_ExtractsCorrectVat()
{
// Arrange - Sale of 1250 kr incl. VAT (1000 + 250 VAT)
var lines = new List
{
new(1, "1000", 0m, 1250m, "Salg inkl moms", "U25")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Inclusive, InputVatAccount, OutputVatAccount);
// Assert
var vatLine = result.VatLines[0];
vatLine.CreditAmount.Should().Be(250m); // 1250 * 0.25 / 1.25 = 250
vatLine.DebitAmount.Should().Be(0m);
result.VatSummary[0].BaseAmount.Should().Be(1000m); // 1250 - 250
result.VatSummary[0].VatAmount.Should().Be(250m);
}
#endregion
#region Purchases (I25) Tests
[Fact]
public void CalculateVat_PurchaseWithI25_Exclusive_CalculatesCorrectVat()
{
// Arrange - Purchase of 1000 kr excl. VAT
var lines = new List
{
new(1, "2000", 1000m, 0m, "Køb af varer", "I25") // Debit = expense
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
result.VatLines.Should().HaveCount(1);
var vatLine = result.VatLines[0];
vatLine.DebitAmount.Should().Be(250m); // 25% of 1000 - debit because it's an asset (receivable)
vatLine.CreditAmount.Should().Be(0m);
vatLine.AccountId.Should().Be(InputVatAccount);
vatLine.VatCode.Should().Be("I25");
result.VatSummary[0].IsInputVat.Should().BeTrue();
}
[Fact]
public void CalculateVat_PurchaseWithI25_Inclusive_ExtractsCorrectVat()
{
// Arrange - Purchase of 1250 kr incl. VAT
var lines = new List
{
new(1, "2000", 1250m, 0m, "Køb inkl moms", "I25")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Inclusive, InputVatAccount, OutputVatAccount);
// Assert
var vatLine = result.VatLines[0];
vatLine.DebitAmount.Should().Be(250m);
vatLine.CreditAmount.Should().Be(0m);
result.VatSummary[0].BaseAmount.Should().Be(1000m);
result.VatSummary[0].VatAmount.Should().Be(250m);
}
#endregion
#region No VAT Tests
[Fact]
public void CalculateVat_NoVatCode_ReturnsNoVatLines()
{
// Arrange
var lines = new List
{
new(1, "1000", 0m, 1000m, "Momsfrit salg") // No VAT code
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
result.VatLines.Should().BeEmpty();
result.VatSummary.Should().BeEmpty();
result.TotalVatAmount.Should().Be(0m);
}
[Fact]
public void CalculateVat_WithINGEN_ReturnsNoVatLines()
{
// Arrange
var lines = new List
{
new(1, "1000", 0m, 1000m, "Momsfrit", "INGEN")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
result.VatLines.Should().BeEmpty();
}
[Fact]
public void CalculateVat_WithUEU_ReturnsNoVatLines()
{
// Arrange - EU sale (no VAT charged)
var lines = new List
{
new(1, "1000", 0m, 10000m, "EU-salg af varer", "UEU")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert - UEU has 0% rate, so no VAT line generated
result.VatLines.Should().BeEmpty();
}
#endregion
#region Multiple Lines Tests
[Fact]
public void CalculateVat_MultipleLines_AggregatesByVatCode()
{
// Arrange
var lines = new List
{
new(1, "1000", 0m, 1000m, "Salg 1", "U25"),
new(2, "1000", 0m, 2000m, "Salg 2", "U25"),
new(3, "2000", 500m, 0m, "Køb 1", "I25")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
result.VatLines.Should().HaveCount(3);
// Check VAT summary aggregates correctly
result.VatSummary.Should().HaveCount(2); // U25 and I25
var u25Summary = result.VatSummary.First(s => s.VatCode == "U25");
u25Summary.BaseAmount.Should().Be(3000m); // 1000 + 2000
u25Summary.VatAmount.Should().Be(750m); // 25% of 3000
var i25Summary = result.VatSummary.First(s => s.VatCode == "I25");
i25Summary.BaseAmount.Should().Be(500m);
i25Summary.VatAmount.Should().Be(125m); // 25% of 500
}
#endregion
#region Rounding Tests
[Fact]
public void CalculateVat_OddAmount_RoundsCorrectly()
{
// Arrange - Amount that results in rounding
var lines = new List
{
new(1, "1000", 0m, 100.01m, "Salg med øreafrunding", "U25")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
// 100.01 * 0.25 = 25.0025, rounded to 25.00
result.VatLines[0].CreditAmount.Should().Be(25.00m);
}
[Fact]
public void CalculateVat_InclusiveOddAmount_RoundsCorrectly()
{
// Arrange - 125.01 incl VAT
var lines = new List
{
new(1, "1000", 0m, 125.01m, "Salg inkl moms", "U25")
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Inclusive, InputVatAccount, OutputVatAccount);
// Assert
// 125.01 * 0.25 / 1.25 = 25.002, rounded to 25.00
result.VatLines[0].CreditAmount.Should().Be(25.00m);
result.VatSummary[0].BaseAmount.Should().Be(100.01m); // 125.01 - 25.00
}
#endregion
#region Total VAT Tests
[Fact]
public void CalculateVat_MixedInputOutput_CalculatesTotalCorrectly()
{
// Arrange - Sales and purchases
var lines = new List
{
new(1, "1000", 0m, 1000m, "Salg", "U25"), // Output VAT: 250 credit
new(2, "2000", 400m, 0m, "Køb", "I25") // Input VAT: 100 debit
};
// Act
var result = _sut.CalculateVat(lines, VatCalculationMode.Exclusive, InputVatAccount, OutputVatAccount);
// Assert
// Total = debit (input VAT asset) - credit (output VAT liability)
// = 100 - 250 = -150 (negative means net VAT payable to SKAT)
result.TotalVatAmount.Should().Be(-150m);
}
#endregion
}