books/backend/Books.Api.Tests/Invoicing/InvoicePdfGeneratorTests.cs
Nicolaj Hartmann 1f75c5d791 Add all backend domain, commands, repositories, and tests
This commit includes all previously untracked backend files:

Domain:
- Accounts, Attachments, BankConnections, Customers
- FiscalYears, Invoices, JournalEntryDrafts
- Orders, Products, UserAccess

Commands & Handlers:
- Full CQRS command structure for all domains

Repositories:
- PostgreSQL repositories for all read models
- Bank transaction and ledger repositories

GraphQL:
- Input types, scalars, and types for all entities
- Mutations and queries

Infrastructure:
- Banking integration (Enable Banking client)
- File storage, Invoicing, Reporting, SAF-T export
- Database migrations (003-029)

Tests:
- Integration tests for GraphQL endpoints
- Domain tests
- Invoicing and reporting tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 22:19:42 +01:00

484 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
}