2026-02-05 21:35:26 +01:00
|
|
|
using Books.Api.Domain;
|
|
|
|
|
|
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
|
|
|
namespace Books.Api.Domain.Invoices;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Value object representing a single line on an invoice.
|
|
|
|
|
/// Contains product/service description, quantity, pricing, and VAT information.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed record InvoiceLine
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Line number for ordering (1-based).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public int LineNumber { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Description of the product or service.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string Description { get; init; } = string.Empty;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Quantity (can be decimal for services billed by hour).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal Quantity { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Unit of measurement (e.g., "stk", "timer", "kg").
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string? Unit { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Price per unit excluding VAT.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal UnitPrice { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Discount percentage (0-100).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal DiscountPercent { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// VAT code (e.g., "U25" for 25%, "UEU" for EU sales, "UEXP" for export).
|
|
|
|
|
/// Uses existing VatCode definitions.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string VatCode { get; init; } = "U25";
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Revenue account to credit when invoice is sent.
|
|
|
|
|
/// If null, uses customer's default revenue account.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public string? AccountId { get; init; }
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Calculated: UnitPrice * Quantity * (1 - DiscountPercent/100)
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal AmountExVat => Math.Round(UnitPrice * Quantity * (1 - DiscountPercent / 100m), 2);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Calculated VAT amount based on VatCode.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal AmountVat => Math.Round(AmountExVat * GetVatRate(), 2);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Calculated: AmountExVat + AmountVat
|
|
|
|
|
/// </summary>
|
|
|
|
|
public decimal AmountTotal => AmountExVat + AmountVat;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Gets the VAT rate for this line based on VatCode.
|
2026-02-05 21:35:26 +01:00
|
|
|
/// Delegates to the canonical VatCodes.GetRate() to ensure consistency.
|
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
|
|
|
/// </summary>
|
2026-02-05 21:35:26 +01:00
|
|
|
private decimal GetVatRate()
|
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
|
|
|
{
|
2026-02-05 21:35:26 +01:00
|
|
|
if (!VatCodes.IsValid(VatCode))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Unknown VAT code '{VatCode}' on invoice line {LineNumber}. " +
|
|
|
|
|
$"Valid codes: U25, UEU, UEXP, I25, IEUV, IEUY, IVV, IVY, REP, INGEN");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return VatCodes.GetRate(VatCode);
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Creates an InvoiceLine with validation.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static InvoiceLine Create(
|
|
|
|
|
int lineNumber,
|
|
|
|
|
string description,
|
|
|
|
|
decimal quantity,
|
|
|
|
|
decimal unitPrice,
|
|
|
|
|
string? vatCode = null,
|
|
|
|
|
string? accountId = null,
|
|
|
|
|
string? unit = null,
|
|
|
|
|
decimal discountPercent = 0)
|
|
|
|
|
{
|
|
|
|
|
if (lineNumber < 1)
|
|
|
|
|
throw new ArgumentException("Line number must be at least 1", nameof(lineNumber));
|
|
|
|
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(description))
|
|
|
|
|
throw new ArgumentException("Description is required", nameof(description));
|
|
|
|
|
|
|
|
|
|
if (quantity <= 0)
|
|
|
|
|
throw new ArgumentException("Quantity must be positive", nameof(quantity));
|
|
|
|
|
|
|
|
|
|
if (unitPrice < 0)
|
|
|
|
|
throw new ArgumentException("Unit price cannot be negative", nameof(unitPrice));
|
|
|
|
|
|
|
|
|
|
if (discountPercent < 0 || discountPercent > 100)
|
|
|
|
|
throw new ArgumentException("Discount must be between 0 and 100", nameof(discountPercent));
|
|
|
|
|
|
|
|
|
|
return new InvoiceLine
|
|
|
|
|
{
|
|
|
|
|
LineNumber = lineNumber,
|
|
|
|
|
Description = description.Trim(),
|
|
|
|
|
Quantity = quantity,
|
|
|
|
|
UnitPrice = unitPrice,
|
|
|
|
|
VatCode = vatCode ?? "U25",
|
|
|
|
|
AccountId = accountId,
|
|
|
|
|
Unit = unit,
|
|
|
|
|
DiscountPercent = discountPercent
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|