using Books.Api.Domain; using Books.Api.Domain.FiscalYears; using Books.Api.EventFlow.Repositories; using EventFlow.Commands; namespace Books.Api.Commands.FiscalYears; /// /// Command handler for creating a new fiscal year. /// Validates overlap with existing fiscal years and checks for gaps. /// public class CreateFiscalYearCommandHandler( IFiscalYearRepository fiscalYearRepository) : CommandHandler { public override async Task ExecuteAsync( FiscalYearAggregate aggregate, CreateFiscalYearCommand command, CancellationToken cancellationToken) { // Check for overlapping fiscal years var hasOverlap = await fiscalYearRepository.HasOverlappingYearAsync( command.CompanyId, command.StartDate, command.EndDate, excludeId: null, cancellationToken); if (hasOverlap) { throw new DomainException( "FISCAL_YEAR_OVERLAP", "The fiscal year overlaps with an existing fiscal year", "Regnskabsåret overlapper med et eksisterende regnskabsår"); } // Check for gaps: verify the new start date follows the latest existing fiscal year if (!command.IsFirstFiscalYear) { var existingYears = await fiscalYearRepository.GetByCompanyIdAsync( command.CompanyId, cancellationToken); if (existingYears.Count > 0) { // Find the latest end date among existing fiscal years var latestEndDate = existingYears .Select(fy => DateOnly.FromDateTime(fy.EndDate)) .Max(); var expectedStartDate = latestEndDate.AddDays(1); if (command.StartDate != expectedStartDate) { throw new DomainException( "FISCAL_YEAR_GAP", $"Fiscal year must start on {expectedStartDate:yyyy-MM-dd} (day after previous year ends on {latestEndDate:yyyy-MM-dd}). No gaps are allowed between fiscal years.", $"Regnskabsåret skal starte den {expectedStartDate:yyyy-MM-dd} (dagen efter forrige år slutter den {latestEndDate:yyyy-MM-dd}). Der må ikke være huller mellem regnskabsår."); } } } aggregate.Create( command.CompanyId, command.Name, command.StartDate, command.EndDate, command.IsFirstFiscalYear, command.IsReorganization); } } public class CloseFiscalYearCommandHandler : CommandHandler { public override Task ExecuteAsync( FiscalYearAggregate aggregate, CloseFiscalYearCommand command, CancellationToken cancellationToken) { aggregate.Close(command.ClosedBy); return Task.CompletedTask; } } public class ReopenFiscalYearCommandHandler : CommandHandler { public override Task ExecuteAsync( FiscalYearAggregate aggregate, ReopenFiscalYearCommand command, CancellationToken cancellationToken) { aggregate.Reopen(command.ReopenedBy); return Task.CompletedTask; } } public class LockFiscalYearCommandHandler : CommandHandler { public override Task ExecuteAsync( FiscalYearAggregate aggregate, LockFiscalYearCommand command, CancellationToken cancellationToken) { aggregate.Lock(command.LockedBy); return Task.CompletedTask; } } public class MarkOpeningBalancePostedCommandHandler : CommandHandler { public override Task ExecuteAsync( FiscalYearAggregate aggregate, MarkOpeningBalancePostedCommand command, CancellationToken cancellationToken) { aggregate.MarkOpeningBalancePosted(); return Task.CompletedTask; } }