266 lines
8.2 KiB
C#
266 lines
8.2 KiB
C#
|
|
using Books.Api.Domain;
|
||
|
|
using Books.Api.Domain.JournalEntryDrafts;
|
||
|
|
using AwesomeAssertions;
|
||
|
|
|
||
|
|
namespace Books.Api.Tests.Domain;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Unit tests for VatCalculationService.
|
||
|
|
/// Tests Danish VAT calculation logic for SKAT compliance.
|
||
|
|
/// </summary>
|
||
|
|
[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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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<DraftLine>
|
||
|
|
{
|
||
|
|
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
|
||
|
|
}
|