using Books.Api.Domain; using Books.Api.Domain.Invoices; using Books.Api.EventFlow.Repositories; using Books.Api.Invoicing.Services; using EventFlow.Commands; namespace Books.Api.Commands.Invoices; /// /// Command handler for creating invoices. /// Always auto-assigns a sequential invoice number (Momsloven §52 - sequential numbering required). /// Validates the company has a CVR number (required for invoicing). /// public class CreateInvoiceCommandHandler( IInvoiceNumberService invoiceNumberService, ICompanyRepository companyRepository) : CommandHandler { public override async Task ExecuteAsync( InvoiceAggregate aggregate, CreateInvoiceCommand command, CancellationToken cancellationToken) { // Validate company has a CVR number (required for invoicing per Danish law) var company = await companyRepository.GetByIdAsync(command.CompanyId, cancellationToken); if (company == null) { throw new DomainException( "COMPANY_NOT_FOUND", $"Company '{command.CompanyId}' not found", $"Virksomheden '{command.CompanyId}' blev ikke fundet"); } if (string.IsNullOrWhiteSpace(company.Cvr)) { throw new DomainException( "CVR_REQUIRED_FOR_INVOICE", "Company must have a CVR number to create invoices. Please update company settings.", "Virksomheden skal have et CVR-nummer for at oprette fakturaer. Opdater venligst virksomhedsindstillinger."); } // Always auto-assign invoice number (Momsloven §52 requires unbroken sequential numbering) var invoiceNumber = await invoiceNumberService.GetNextInvoiceNumberAsync( command.CompanyId, command.InvoiceDate.Year, cancellationToken); // Build seller address from company info var sellerAddress = string.Join(", ", new[] { company.Address, company.PostalCode, company.City } .Where(s => !string.IsNullOrWhiteSpace(s))); aggregate.Create( command.CompanyId, command.FiscalYearId, command.CustomerId, command.CustomerName, command.CustomerNumber, invoiceNumber, command.InvoiceDate, command.DueDate, command.PaymentTermsDays, command.Currency, command.VatCode, command.Notes, command.Reference, command.CreatedBy, company.Cvr, company.Name, sellerAddress); } } public class AddInvoiceLineCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, AddInvoiceLineCommand command, CancellationToken cancellationToken) { aggregate.AddLine( command.Description, command.Quantity, command.UnitPrice, command.VatCode, command.AccountId, command.Unit, command.DiscountPercent); return Task.CompletedTask; } } public class UpdateInvoiceLineCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, UpdateInvoiceLineCommand command, CancellationToken cancellationToken) { aggregate.UpdateLine( command.LineNumber, command.Description, command.Quantity, command.UnitPrice, command.VatCode, command.AccountId, command.Unit, command.DiscountPercent); return Task.CompletedTask; } } public class RemoveInvoiceLineCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, RemoveInvoiceLineCommand command, CancellationToken cancellationToken) { aggregate.RemoveLine(command.LineNumber); return Task.CompletedTask; } } /// /// Command handler for marking an invoice as sent. /// Validates fiscal year status before allowing the invoice to be sent. /// public class MarkInvoiceSentCommandHandler( IInvoiceRepository invoiceRepository, IFiscalYearRepository fiscalYearRepository) : CommandHandler { public override async Task ExecuteAsync( InvoiceAggregate aggregate, MarkInvoiceSentCommand command, CancellationToken cancellationToken) { // Load the invoice read model to get fiscal year ID var invoice = await invoiceRepository.GetByIdAsync( aggregate.Id.Value, cancellationToken); var fiscalYearId = invoice?.FiscalYearId; // Validate fiscal year is set if (string.IsNullOrWhiteSpace(fiscalYearId)) { throw new DomainException( "FISCAL_YEAR_REQUIRED", "Fiscal year is required for sending an invoice", "Regnskabsår er påkrævet for afsendelse af en faktura"); } // Fetch and validate fiscal year var fiscalYear = await fiscalYearRepository.GetByIdAsync( fiscalYearId, cancellationToken); if (fiscalYear == null) { throw new DomainException( "FISCAL_YEAR_NOT_FOUND", $"Fiscal year '{fiscalYearId}' not found", $"Regnskabsår '{fiscalYearId}' blev ikke fundet"); } // Validate fiscal year is open (not Closed or Locked) if (fiscalYear.Status != "Open") { throw new DomainException( "FISCAL_YEAR_NOT_OPEN", $"Fiscal year is {fiscalYear.Status}. Only open fiscal years allow posting.", $"Regnskabsåret er {fiscalYear.Status}. Kun åbne regnskabsår tillader bogføring."); } aggregate.Send( command.LedgerTransactionId, command.SentBy); } } public class ReceiveInvoicePaymentCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, ReceiveInvoicePaymentCommand command, CancellationToken cancellationToken) { aggregate.ReceivePayment( command.Amount, command.BankTransactionId, command.LedgerTransactionId, command.PaymentReference, command.PaymentDate, command.RecordedBy); return Task.CompletedTask; } } public class VoidInvoiceCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, VoidInvoiceCommand command, CancellationToken cancellationToken) { aggregate.Void( command.Reason, command.ReversalLedgerTransactionId, command.VoidedBy); return Task.CompletedTask; } } // ===================================================== // CREDIT NOTE COMMAND HANDLERS // ===================================================== public class CreateCreditNoteCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, CreateCreditNoteCommand command, CancellationToken cancellationToken) { aggregate.CreateCreditNote( command.CompanyId, command.FiscalYearId, command.CustomerId, command.CustomerName, command.CustomerNumber, command.CreditNoteNumber, command.CreditNoteDate, command.Currency, command.VatCode, command.Notes, command.Reference, command.CreatedBy, command.OriginalInvoiceId, command.OriginalInvoiceNumber, command.CreditReason); return Task.CompletedTask; } } public class IssueCreditNoteCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, IssueCreditNoteCommand command, CancellationToken cancellationToken) { aggregate.Issue( command.LedgerTransactionId, command.IssuedBy); return Task.CompletedTask; } } public class ApplyCreditNoteCommandHandler : CommandHandler { public override Task ExecuteAsync( InvoiceAggregate aggregate, ApplyCreditNoteCommand command, CancellationToken cancellationToken) { aggregate.ApplyCredit( command.TargetInvoiceId, command.TargetInvoiceNumber, command.Amount, command.AppliedDate, command.AppliedBy, command.LedgerTransactionId); return Task.CompletedTask; } }