using Books.Api.EventFlow.Repositories; using Ledger.Core.Models; using Ledger.Core.Services; namespace Books.Api.Reporting; /// /// Service for generating VAT (moms) reports from ledger data. /// Aggregates VAT account balances for SKAT compliance. /// public class VatReportService( IAccountRepository accountRepository, ILedgerService ledgerService, ILogger logger) : IVatReportService { // Standard Danish VAT account numbers // TODO: These should ideally come from company-level configuration, // as different chart-of-accounts templates may use different numbers. private const string InputVatAccountNumber = "5610"; // Købsmoms (indgående moms) private const string OutputVatAccountNumber = "5611"; // Salgsmoms (udgående moms) public async Task GenerateReportAsync( string companyId, DateOnly periodStart, DateOnly periodEnd, CancellationToken ct = default) { // Validate period if (periodEnd < periodStart) { throw new ArgumentException("Slutdato skal være efter startdato", nameof(periodEnd)); } var daysDiff = periodEnd.DayNumber - periodStart.DayNumber; if (daysDiff > 366) { throw new ArgumentException("Perioden må ikke overstige ét år (366 dage)", nameof(periodEnd)); } logger.LogInformation( "Generating VAT report for company {CompanyId} from {PeriodStart} to {PeriodEnd}", companyId, periodStart, periodEnd); // Look up VAT accounts var inputVatAccount = await accountRepository.GetByCompanyAndNumberAsync( companyId, InputVatAccountNumber, ct); var outputVatAccount = await accountRepository.GetByCompanyAndNumberAsync( companyId, OutputVatAccountNumber, ct); var report = new VatReportDto { PeriodStart = periodStart, PeriodEnd = periodEnd }; // If neither VAT account exists, return empty report if (inputVatAccount == null && outputVatAccount == null) { logger.LogWarning( "No VAT accounts found for company {CompanyId}. Returning empty report.", companyId); return report; } // Collect account IDs for ledger query var accountIds = new List(); if (inputVatAccount != null && TryParseAccountGuid(inputVatAccount.Id, out var inputGuid)) { accountIds.Add(inputGuid); } if (outputVatAccount != null && TryParseAccountGuid(outputVatAccount.Id, out var outputGuid)) { accountIds.Add(outputGuid); } if (accountIds.Count == 0) { logger.LogWarning("No valid VAT account GUIDs found for company {CompanyId}", companyId); return report; } // Query ledger for aggregated balances in the period var query = new EntriesQuery { AccountIds = accountIds, From = new DateTimeOffset(periodStart.ToDateTime(TimeOnly.MinValue)), To = new DateTimeOffset(periodEnd.ToDateTime(TimeOnly.MaxValue)), Aggregate = true }; var result = await ledgerService.QueryEntriesAsync(query, ct); // Process aggregated balances if (result.Aggregates != null) { foreach (var balance in result.Aggregates) { if (inputVatAccount != null && TryParseAccountGuid(inputVatAccount.Id, out var checkInputGuid) && balance.AccountId == checkInputGuid) { // Input VAT (5610): Debits are VAT receivable/deductions // Box B = total input VAT deduction report.BoxB = balance.TotalDebits; report.TransactionCount += balance.EntryCount; logger.LogDebug( "Input VAT (5610): TotalDebits={Debits}, TotalCredits={Credits}, NetChange={Net}", balance.TotalDebits, balance.TotalCredits, balance.NetChange); } else if (outputVatAccount != null && TryParseAccountGuid(outputVatAccount.Id, out var checkOutputGuid) && balance.AccountId == checkOutputGuid) { // Output VAT (5611): Credits are VAT payable to SKAT // Box A = total output VAT (salgsmoms) report.BoxA = balance.TotalCredits; report.TransactionCount += balance.EntryCount; logger.LogDebug( "Output VAT (5611): TotalDebits={Debits}, TotalCredits={Credits}, NetChange={Net}", balance.TotalDebits, balance.TotalCredits, balance.NetChange); } } } // Calculate summary totals // Box A = Salgsmoms (domestic output VAT) // Box B = Købsmoms (input VAT - deductible) // Box C = EU-varekøb moms (not yet supported - requires VAT code breakdown) // Box D = Ydelseskøb moms (not yet supported - requires VAT code breakdown) report.TotalOutputVat = report.BoxA + report.BoxC + report.BoxD; report.TotalInputVat = report.BoxB; report.NetVat = report.TotalOutputVat - report.TotalInputVat; // Basis1 (Felt 1): Net domestic turnover with VAT // TODO: Query actual net turnover from transactions with output VAT codes (U25) // instead of back-calculating from VAT amount, which is inaccurate when // mixed VAT rates or partial deductions are involved. // Ideally: query revenue account balances filtered by VAT code U25. // For now, back-calculate from output VAT assuming standard 25% rate if (report.BoxA > 0) { report.Basis1 = Math.Round(report.BoxA / 0.25m, 2); } // TODO: Box C (EU-varekøb moms) - Requires VAT code breakdown from transactions. // Query transactions with VAT code IEUV to compute reverse-charge VAT on EU goods. // report.BoxC = sum of VAT calculated on IEUV transactions. // report.Basis3 = net purchase amount for IEUV transactions. // TODO: Box D (Ydelseskøb moms) - Requires VAT code breakdown from transactions. // Query transactions with VAT codes IEUY, IVV, IVY to compute reverse-charge VAT // on services purchased from abroad. // report.BoxD = sum of VAT calculated on IEUY/IVV/IVY transactions. // report.Basis4 = net purchase amount for IEUY/IVV/IVY transactions. logger.LogInformation( "VAT report generated for company {CompanyId}: OutputVAT={OutputVat}, InputVAT={InputVat}, NetVAT={NetVat}", companyId, report.TotalOutputVat, report.TotalInputVat, report.NetVat); return report; } private static bool TryParseAccountGuid(string accountId, out Guid guid) { // Account IDs are in format "account-{guid}" if (accountId.StartsWith("account-")) { return Guid.TryParse(accountId.Replace("account-", ""), out guid); } guid = Guid.Empty; return false; } }