485 lines
14 KiB
C#
485 lines
14 KiB
C#
|
|
using Books.Api.Invoicing.Pdf;
|
||
|
|
using AwesomeAssertions;
|
||
|
|
using QuestPDF.Infrastructure;
|
||
|
|
|
||
|
|
namespace Books.Api.Tests.Invoicing;
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// Unit tests for the InvoicePdfGenerator.
|
||
|
|
/// </summary>
|
||
|
|
[Trait("Category", "Unit")]
|
||
|
|
public class InvoicePdfGeneratorTests
|
||
|
|
{
|
||
|
|
private readonly InvoicePdfGenerator _generator;
|
||
|
|
|
||
|
|
public InvoicePdfGeneratorTests()
|
||
|
|
{
|
||
|
|
// Ensure QuestPDF license is set for tests
|
||
|
|
QuestPDF.Settings.License = LicenseType.Community;
|
||
|
|
_generator = new InvoicePdfGenerator();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithValidData_ProducesPdfBytes()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
pdfBytes.Should().StartWith([0x25, 0x50, 0x44, 0x46]); // PDF magic bytes "%PDF"
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithMultipleLines_ProducesPdf()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData(lineCount: 10);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
pdfBytes.Length.Should().BeGreaterThan(1000); // PDF should be substantial
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithSingleLine_ProducesPdf()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData(lineCount: 1);
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithDiscount_ProducesPdf()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Lines =
|
||
|
|
[
|
||
|
|
new InvoicePdfLineItem
|
||
|
|
{
|
||
|
|
LineNumber = 1,
|
||
|
|
Description = "Produkt med rabat",
|
||
|
|
Quantity = 10,
|
||
|
|
Unit = "stk",
|
||
|
|
UnitPrice = 100m,
|
||
|
|
DiscountPercent = 15m,
|
||
|
|
VatCode = "U25",
|
||
|
|
AmountExVat = 850m,
|
||
|
|
AmountVat = 212.50m,
|
||
|
|
AmountTotal = 1062.50m
|
||
|
|
}
|
||
|
|
],
|
||
|
|
AmountExVat = 850m,
|
||
|
|
AmountVat = 212.50m,
|
||
|
|
AmountTotal = 1062.50m,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine
|
||
|
|
{
|
||
|
|
VatCode = "U25",
|
||
|
|
VatRate = 0.25m,
|
||
|
|
BasisAmount = 850m,
|
||
|
|
VatAmount = 212.50m
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithMultipleVatCodes_ShowsVatSummary()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateDataWithMixedVatCodes();
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithDanishCharacters_HandlesEncoding()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
CompanyName = "Møller & Sønner ApS",
|
||
|
|
CustomerName = "Børge Åkeson",
|
||
|
|
Notes = "Tak for ordren! Vi ses igen. Æbler, øl og åben dør."
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithLongDescription_HandlesWrapping()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Lines =
|
||
|
|
[
|
||
|
|
new InvoicePdfLineItem
|
||
|
|
{
|
||
|
|
LineNumber = 1,
|
||
|
|
Description = "Dette er en meget lang beskrivelse af en ydelse der inkluderer mange detaljer om hvad der er leveret, hvornår det er leveret, og hvordan det er udført. Beskrivelsen fortsætter for at teste tekst-wrapping funktionaliteten i PDF generatoren.",
|
||
|
|
Quantity = 1,
|
||
|
|
Unit = "stk",
|
||
|
|
UnitPrice = 5000m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "U25",
|
||
|
|
AmountExVat = 5000m,
|
||
|
|
AmountVat = 1250m,
|
||
|
|
AmountTotal = 6250m
|
||
|
|
}
|
||
|
|
],
|
||
|
|
AmountExVat = 5000m,
|
||
|
|
AmountVat = 1250m,
|
||
|
|
AmountTotal = 6250m,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine { VatCode = "U25", VatRate = 0.25m, BasisAmount = 5000m, VatAmount = 1250m }
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithNotes_IncludesNotesSection()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Notes = "Betalingsfrist overholdes venligst. Ved forsinket betaling beregnes morarenter."
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithReference_IncludesReference()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Reference = "PO-2024-001234"
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithZeroVatLines_HandlesCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Lines =
|
||
|
|
[
|
||
|
|
new InvoicePdfLineItem
|
||
|
|
{
|
||
|
|
LineNumber = 1,
|
||
|
|
Description = "EU-ydelse (momsfri)",
|
||
|
|
Quantity = 1,
|
||
|
|
Unit = "stk",
|
||
|
|
UnitPrice = 10000m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "UEU",
|
||
|
|
AmountExVat = 10000m,
|
||
|
|
AmountVat = 0m,
|
||
|
|
AmountTotal = 10000m
|
||
|
|
}
|
||
|
|
],
|
||
|
|
AmountExVat = 10000m,
|
||
|
|
AmountVat = 0m,
|
||
|
|
AmountTotal = 10000m,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine { VatCode = "UEU", VatRate = 0m, BasisAmount = 10000m, VatAmount = 0m }
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithDecimalQuantity_FormatsCorrectly()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
Lines =
|
||
|
|
[
|
||
|
|
new InvoicePdfLineItem
|
||
|
|
{
|
||
|
|
LineNumber = 1,
|
||
|
|
Description = "Konsulentydelse",
|
||
|
|
Quantity = 7.5m,
|
||
|
|
Unit = "timer",
|
||
|
|
UnitPrice = 850m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "U25",
|
||
|
|
AmountExVat = 6375m,
|
||
|
|
AmountVat = 1593.75m,
|
||
|
|
AmountTotal = 7968.75m
|
||
|
|
}
|
||
|
|
],
|
||
|
|
AmountExVat = 6375m,
|
||
|
|
AmountVat = 1593.75m,
|
||
|
|
AmountTotal = 7968.75m,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine { VatCode = "U25", VatRate = 0.25m, BasisAmount = 6375m, VatAmount = 1593.75m }
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithBankDetails_IncludesPaymentInfo()
|
||
|
|
{
|
||
|
|
// Arrange
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
CompanyBankName = "Nordea",
|
||
|
|
CompanyBankRegNo = "2191",
|
||
|
|
CompanyBankAccountNo = "0012345678",
|
||
|
|
CompanyIban = "DK8920000012345678",
|
||
|
|
CompanyBic = "NDEADKKK"
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
pdfBytes.Should().StartWith([0x25, 0x50, 0x44, 0x46]); // PDF magic bytes
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithPartialBankDetails_ProducesPdf()
|
||
|
|
{
|
||
|
|
// Arrange - Only Danish bank details, no IBAN/BIC
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
CompanyBankName = "Jyske Bank",
|
||
|
|
CompanyBankRegNo = "5064",
|
||
|
|
CompanyBankAccountNo = "1234567",
|
||
|
|
CompanyIban = null,
|
||
|
|
CompanyBic = null
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
[Fact]
|
||
|
|
public void Generate_WithNoBankDetails_ProducesPdf()
|
||
|
|
{
|
||
|
|
// Arrange - No bank details at all
|
||
|
|
var data = CreateSampleInvoiceData() with
|
||
|
|
{
|
||
|
|
CompanyBankName = null,
|
||
|
|
CompanyBankRegNo = null,
|
||
|
|
CompanyBankAccountNo = null,
|
||
|
|
CompanyIban = null,
|
||
|
|
CompanyBic = null
|
||
|
|
};
|
||
|
|
|
||
|
|
// Act
|
||
|
|
var pdfBytes = _generator.Generate(data);
|
||
|
|
|
||
|
|
// Assert
|
||
|
|
pdfBytes.Should().NotBeNullOrEmpty();
|
||
|
|
}
|
||
|
|
|
||
|
|
#region Test Data Helpers
|
||
|
|
|
||
|
|
private static InvoicePdfData CreateSampleInvoiceData(int lineCount = 3)
|
||
|
|
{
|
||
|
|
var lines = Enumerable.Range(1, lineCount)
|
||
|
|
.Select(i => new InvoicePdfLineItem
|
||
|
|
{
|
||
|
|
LineNumber = i,
|
||
|
|
Description = $"Konsulentydelse - uge {i}",
|
||
|
|
Quantity = 40,
|
||
|
|
Unit = "timer",
|
||
|
|
UnitPrice = 850m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "U25",
|
||
|
|
AmountExVat = 34000m,
|
||
|
|
AmountVat = 8500m,
|
||
|
|
AmountTotal = 42500m
|
||
|
|
})
|
||
|
|
.ToList();
|
||
|
|
|
||
|
|
var totalExVat = lines.Sum(l => l.AmountExVat);
|
||
|
|
var totalVat = lines.Sum(l => l.AmountVat);
|
||
|
|
var total = lines.Sum(l => l.AmountTotal);
|
||
|
|
|
||
|
|
return new InvoicePdfData
|
||
|
|
{
|
||
|
|
CompanyName = "Test Firma ApS",
|
||
|
|
CompanyCvr = "12345678",
|
||
|
|
CompanyAddress = "Testvej 123",
|
||
|
|
CompanyPostalCode = "2100",
|
||
|
|
CompanyCity = "København Ø",
|
||
|
|
CompanyCountry = "DK",
|
||
|
|
|
||
|
|
// Bank details
|
||
|
|
CompanyBankName = "Danske Bank",
|
||
|
|
CompanyBankRegNo = "3409",
|
||
|
|
CompanyBankAccountNo = "1234567890",
|
||
|
|
CompanyIban = "DK5000400440116243",
|
||
|
|
CompanyBic = "DABADKKK",
|
||
|
|
|
||
|
|
CustomerName = "Kunde ApS",
|
||
|
|
CustomerCvr = "87654321",
|
||
|
|
CustomerAddress = "Kundevej 456",
|
||
|
|
CustomerPostalCode = "8000",
|
||
|
|
CustomerCity = "Aarhus C",
|
||
|
|
CustomerCountry = "DK",
|
||
|
|
CustomerNumber = "K-001",
|
||
|
|
|
||
|
|
InvoiceNumber = "2024-0001",
|
||
|
|
InvoiceDate = new DateOnly(2024, 1, 15),
|
||
|
|
DueDate = new DateOnly(2024, 2, 14),
|
||
|
|
PaymentTermsDays = 30,
|
||
|
|
Currency = "DKK",
|
||
|
|
|
||
|
|
Lines = lines,
|
||
|
|
AmountExVat = totalExVat,
|
||
|
|
AmountVat = totalVat,
|
||
|
|
AmountTotal = total,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine
|
||
|
|
{
|
||
|
|
VatCode = "U25",
|
||
|
|
VatRate = 0.25m,
|
||
|
|
BasisAmount = totalExVat,
|
||
|
|
VatAmount = totalVat
|
||
|
|
}
|
||
|
|
]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static InvoicePdfData CreateDataWithMixedVatCodes()
|
||
|
|
{
|
||
|
|
var lines = new List<InvoicePdfLineItem>
|
||
|
|
{
|
||
|
|
new()
|
||
|
|
{
|
||
|
|
LineNumber = 1,
|
||
|
|
Description = "Dansk ydelse (25% moms)",
|
||
|
|
Quantity = 1,
|
||
|
|
Unit = "stk",
|
||
|
|
UnitPrice = 10000m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "U25",
|
||
|
|
AmountExVat = 10000m,
|
||
|
|
AmountVat = 2500m,
|
||
|
|
AmountTotal = 12500m
|
||
|
|
},
|
||
|
|
new()
|
||
|
|
{
|
||
|
|
LineNumber = 2,
|
||
|
|
Description = "EU-ydelse (0% moms)",
|
||
|
|
Quantity = 1,
|
||
|
|
Unit = "stk",
|
||
|
|
UnitPrice = 5000m,
|
||
|
|
DiscountPercent = 0,
|
||
|
|
VatCode = "UEU",
|
||
|
|
AmountExVat = 5000m,
|
||
|
|
AmountVat = 0m,
|
||
|
|
AmountTotal = 5000m
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
return new InvoicePdfData
|
||
|
|
{
|
||
|
|
CompanyName = "Mixed VAT Company ApS",
|
||
|
|
CompanyCvr = "11111111",
|
||
|
|
CompanyAddress = "Momsvej 1",
|
||
|
|
CompanyPostalCode = "1000",
|
||
|
|
CompanyCity = "København K",
|
||
|
|
CompanyCountry = "DK",
|
||
|
|
|
||
|
|
CustomerName = "EU Customer GmbH",
|
||
|
|
CustomerCvr = "DE123456789",
|
||
|
|
CustomerAddress = "Hauptstraße 1",
|
||
|
|
CustomerPostalCode = "10115",
|
||
|
|
CustomerCity = "Berlin",
|
||
|
|
CustomerCountry = "DE",
|
||
|
|
CustomerNumber = "K-EU-001",
|
||
|
|
|
||
|
|
InvoiceNumber = "2024-0002",
|
||
|
|
InvoiceDate = new DateOnly(2024, 2, 1),
|
||
|
|
DueDate = new DateOnly(2024, 3, 1),
|
||
|
|
PaymentTermsDays = 30,
|
||
|
|
Currency = "DKK",
|
||
|
|
|
||
|
|
Lines = lines,
|
||
|
|
AmountExVat = 15000m,
|
||
|
|
AmountVat = 2500m,
|
||
|
|
AmountTotal = 17500m,
|
||
|
|
VatSummary =
|
||
|
|
[
|
||
|
|
new VatSummaryLine { VatCode = "U25", VatRate = 0.25m, BasisAmount = 10000m, VatAmount = 2500m },
|
||
|
|
new VatSummaryLine { VatCode = "UEU", VatRate = 0m, BasisAmount = 5000m, VatAmount = 0m }
|
||
|
|
]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
#endregion
|
||
|
|
}
|