using Books.Api.Domain.JournalEntryDrafts; namespace Books.Api.Domain; /// /// Service for calculating VAT (moms) on journal entry lines. /// Handles Danish VAT rules including automatic calculation of VAT amounts /// and generation of VAT posting lines. /// public interface IVatCalculationService { /// /// Calculate VAT for a list of draft lines. /// Returns the original lines plus generated VAT posting lines. /// VatCalculationResult CalculateVat( IEnumerable lines, VatCalculationMode mode, string inputVatAccountId, string outputVatAccountId); } /// /// Determines how amounts on lines should be interpreted. /// public enum VatCalculationMode { /// /// Amounts are exclusive of VAT (ekskl. moms). /// VAT will be added to the total. /// Exclusive, /// /// Amounts are inclusive of VAT (inkl. moms). /// VAT will be extracted from the total. /// Inclusive } /// /// Result of VAT calculation containing original lines, VAT lines, and totals. /// public class VatCalculationResult { /// /// The original lines (unchanged). /// public required List OriginalLines { get; init; } /// /// Generated VAT posting lines (to be added to the journal entry). /// public required List VatLines { get; init; } /// /// Breakdown of VAT by code for reporting purposes. /// public required List VatSummary { get; init; } /// /// Total VAT amount (sum of all VAT lines). /// public decimal TotalVatAmount { get; init; } } /// /// A generated VAT posting line. /// public record VatPostingLine { public required string AccountId { get; init; } public required decimal DebitAmount { get; init; } public required decimal CreditAmount { get; init; } public required string Description { get; init; } /// /// The VAT code this line relates to. /// public required string VatCode { get; init; } /// /// The original line number this VAT line was calculated from. /// public required int SourceLineNumber { get; init; } } /// /// Summary of VAT for a specific VAT code. /// public record VatSummary { public required string VatCode { get; init; } public required decimal Rate { get; init; } /// /// Total base amount (before VAT) for this VAT code. /// public required decimal BaseAmount { get; init; } /// /// Total VAT amount for this VAT code. /// public required decimal VatAmount { get; init; } /// /// Is this input VAT (købsmoms/fradrag) or output VAT (salgsmoms/skyldig)? /// public required bool IsInputVat { get; init; } } public class VatCalculationService : IVatCalculationService { public VatCalculationResult CalculateVat( IEnumerable lines, VatCalculationMode mode, string inputVatAccountId, string outputVatAccountId) { var linesList = lines.ToList(); var vatLines = new List(); var vatByCode = new Dictionary(); foreach (var line in linesList) { if (string.IsNullOrEmpty(line.VatCode) || line.VatCode == VatCodes.INGEN) continue; var rate = VatCodes.GetRate(line.VatCode); if (rate == 0) continue; // Determine if this is a debit or credit line var isDebit = line.DebitAmount > 0; var amount = isDebit ? line.DebitAmount : line.CreditAmount; // Calculate base and VAT amounts based on mode decimal baseAmount; decimal vatAmount; if (mode == VatCalculationMode.Inclusive) { // Amount includes VAT, extract it // VAT = Amount * rate / (1 + rate) vatAmount = Math.Round(amount * rate / (1 + rate), 2, MidpointRounding.AwayFromZero); baseAmount = amount - vatAmount; } else { // Amount excludes VAT, calculate it baseAmount = amount; vatAmount = Math.Round(amount * rate, 2, MidpointRounding.AwayFromZero); } // Determine if this is input or output VAT var isInputVat = VatCodes.IsInputVat(line.VatCode); var vatAccountId = isInputVat ? inputVatAccountId : outputVatAccountId; // Create VAT posting line // For sales (output VAT): we credit the VAT account // For purchases (input VAT): we debit the VAT account var vatLine = new VatPostingLine { AccountId = vatAccountId, DebitAmount = isInputVat && isDebit ? vatAmount : (isInputVat ? 0 : 0), CreditAmount = !isInputVat && !isDebit ? vatAmount : (!isInputVat && isDebit ? vatAmount : 0), Description = $"Moms {line.VatCode} ({rate * 100:0}%)", VatCode = line.VatCode, SourceLineNumber = line.LineNumber }; // Correct the debit/credit logic: // - For SALES (U25): revenue is credit, VAT should ALSO be credit (liability to SKAT) // - For PURCHASES (I25): expense is debit, VAT should ALSO be debit (asset/receivable from SKAT) // The key insight: VAT follows the same direction as the base transaction vatLine = vatLine with { DebitAmount = isDebit ? vatAmount : 0, CreditAmount = !isDebit ? vatAmount : 0 }; vatLines.Add(vatLine); // Accumulate VAT summary if (!vatByCode.TryGetValue(line.VatCode, out var summary)) { summary = new VatSummary { VatCode = line.VatCode, Rate = rate, BaseAmount = 0, VatAmount = 0, IsInputVat = isInputVat }; vatByCode[line.VatCode] = summary; } vatByCode[line.VatCode] = summary with { BaseAmount = summary.BaseAmount + baseAmount, VatAmount = summary.VatAmount + vatAmount }; } return new VatCalculationResult { OriginalLines = linesList, VatLines = vatLines, VatSummary = vatByCode.Values.ToList(), TotalVatAmount = vatLines.Sum(v => v.DebitAmount - v.CreditAmount) }; } }