using Books.Api.Domain; namespace Books.Api.Domain.Invoices; /// /// Value object representing a single line on an invoice. /// Contains product/service description, quantity, pricing, and VAT information. /// public sealed record InvoiceLine { /// /// Line number for ordering (1-based). /// public int LineNumber { get; init; } /// /// Description of the product or service. /// public string Description { get; init; } = string.Empty; /// /// Quantity (can be decimal for services billed by hour). /// public decimal Quantity { get; init; } /// /// Unit of measurement (e.g., "stk", "timer", "kg"). /// public string? Unit { get; init; } /// /// Price per unit excluding VAT. /// public decimal UnitPrice { get; init; } /// /// Discount percentage (0-100). /// public decimal DiscountPercent { get; init; } /// /// VAT code (e.g., "U25" for 25%, "UEU" for EU sales, "UEXP" for export). /// Uses existing VatCode definitions. /// public string VatCode { get; init; } = "U25"; /// /// Revenue account to credit when invoice is sent. /// If null, uses customer's default revenue account. /// public string? AccountId { get; init; } /// /// Calculated: UnitPrice * Quantity * (1 - DiscountPercent/100) /// public decimal AmountExVat => Math.Round(UnitPrice * Quantity * (1 - DiscountPercent / 100m), 2); /// /// Calculated VAT amount based on VatCode. /// public decimal AmountVat => Math.Round(AmountExVat * GetVatRate(), 2); /// /// Calculated: AmountExVat + AmountVat /// public decimal AmountTotal => AmountExVat + AmountVat; /// /// Gets the VAT rate for this line based on VatCode. /// Delegates to the canonical VatCodes.GetRate() to ensure consistency. /// private decimal GetVatRate() { 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); } /// /// Creates an InvoiceLine with validation. /// 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 }; } }