using System.Text.Json; using General_Ledger_Service.DTOs.Common; using General_Ledger_Service.DTOs.FiscalCalendar; using General_Ledger_Service.Enums; using General_Ledger_Service.Exceptions; using General_Ledger_Service.Helpers; using General_Ledger_Service.Infra.UoW; using General_Ledger_Service.Interfaces.Repositories; using General_Ledger_Service.Interfaces.Services; using General_Ledger_Service.Models; namespace General_Ledger_Service.Services { public class FiscalCalendarService( IFiscalCalendarRepository fiscalCalendarRepository, IAuditLogRepository auditLogRepository, IUnitOfWork unitOfWork) : IFiscalCalendarService { private readonly IFiscalCalendarRepository _fiscalCalendarRepository = fiscalCalendarRepository; private readonly IAuditLogRepository _auditLogRepository = auditLogRepository; private readonly IUnitOfWork _unitOfWork = unitOfWork; public async Task CreateFiscalYearAsync(CreateFiscalYearRequest request) { if (request.EndDate <= request.StartDate) { throw new BadRequestException("End date must be after start date"); } if (await _fiscalCalendarRepository.HasOverlappingFiscalYearRangeAsync(request.StartDate, request.EndDate)) { throw new BadRequestException("Fiscal year date range overlaps with an existing fiscal year"); } var fiscalYear = new FiscalYear { YearName = request.YearName, StartDate = request.StartDate, EndDate = request.EndDate, IsClosed = false }; var generatedPeriods = new List(); await _unitOfWork.BeginAsync(); try { await _fiscalCalendarRepository.AddFiscalYearAsync(fiscalYear); await _unitOfWork.SaveChangesAsync(); if (request.AutoGeneratePeriods) { generatedPeriods = await BuildPeriodsAsync(fiscalYear, request.PeriodCount ?? 12); await _fiscalCalendarRepository.AddPeriodRangeAsync(generatedPeriods); await _unitOfWork.SaveChangesAsync(); } await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Ledger, EntityName = "FiscalYear", EntityId = fiscalYear.FiscalYearId.ToString(), Action = AuditAction.Create, Description = $"Fiscal year {fiscalYear.YearName} created", PerformedBy = null }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Created("Fiscal year created successfully", new CreateFiscalYearResponse { FiscalYear = fiscalYear, GeneratedPeriods = generatedPeriods }); } public async Task GeneratePeriodsAsync(int fiscalYearId, GeneratePeriodsRequest request) { var fiscalYear = await _fiscalCalendarRepository.GetFiscalYearByIdAsync(fiscalYearId) ?? throw new NotFoundException("Fiscal year not found"); var periods = await BuildPeriodsAsync(fiscalYear, request.PeriodCount ?? 12); await _unitOfWork.BeginAsync(); try { await _fiscalCalendarRepository.AddPeriodRangeAsync(periods); await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Ledger, EntityName = "AccountingPeriod", EntityId = fiscalYearId.ToString(), Action = AuditAction.Create, Description = $"{periods.Count} period(s) generated for fiscal year {fiscalYearId}", PerformedBy = null }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Created("Periods generated successfully", periods); } public async Task LockPeriodAsync(int periodId, Dictionary payload) { var rawAction = DataExtractor.GetRequiredString(payload, "action"); var reason = DataExtractor.GetRequiredString(payload, "reason"); if (!Enum.TryParse(rawAction, ignoreCase: true, out var action)) { throw new BadRequestException( $"Invalid action '{rawAction}'. Must be one of: {string.Join(", ", Enum.GetNames())}."); } if (string.IsNullOrWhiteSpace(reason)) { throw new BadRequestException("Reason is required"); } var period = await _fiscalCalendarRepository.GetPeriodByIdAsync(periodId) ?? throw new NotFoundException("Period not found"); if (period.Status == PeriodStatus.Closed && action != PeriodLockAction.Unlock) { throw new BadRequestException("Period is closed; only Unlock is allowed to reopen it"); } (period.Status, period.LockGl) = action switch { PeriodLockAction.Lock => (PeriodStatus.Locked, true), PeriodLockAction.Unlock => (PeriodStatus.Open, false), PeriodLockAction.SoftClose => (PeriodStatus.SoftClose, false), PeriodLockAction.Close => (PeriodStatus.Closed, true), _ => throw new BadRequestException($"Invalid action '{rawAction}'.") }; period.LockReason = reason; var log = new PeriodLockLog { PeriodId = periodId, Action = action, ModuleLocked = "GL", Reason = reason }; var auditAction = action switch { PeriodLockAction.Lock => AuditAction.Lock, PeriodLockAction.Unlock => AuditAction.Unlock, PeriodLockAction.SoftClose => AuditAction.SoftClose, PeriodLockAction.Close => AuditAction.Close, _ => throw new BadRequestException($"Invalid action '{rawAction}'.") }; await _unitOfWork.BeginAsync(); try { await _fiscalCalendarRepository.AddPeriodLockLogAsync(log); await _unitOfWork.SaveChangesAsync(); await _auditLogRepository.AddAsync(new AuditLog { Category = AuditCategory.Ledger, EntityName = "AccountingPeriod", EntityId = periodId.ToString(), Action = auditAction, Description = $"Period {periodId} action {action}", PerformedBy = null }); await _unitOfWork.SaveChangesAsync(); await _unitOfWork.CommitAsync(); } catch { await _unitOfWork.RollbackAsync(); throw; } return ResponseHelpers.Ok("Period lock status updated successfully", new LockPeriodResponse { Period = period, LockLog = log }); } public async Task GetPeriodByDateAsync(DateOnly date) { var period = await _fiscalCalendarRepository.GetPeriodByDateAsync(date) ?? throw new NotFoundException("Period not found"); return ResponseHelpers.Ok("Period retrieved successfully", period); } private async Task> BuildPeriodsAsync(FiscalYear fiscalYear, int periodCount) { if (periodCount <= 0) { throw new BadRequestException("Period count must be greater than zero"); } var existingPeriods = await _fiscalCalendarRepository.GetPeriodsByFiscalYearIdAsync(fiscalYear.FiscalYearId); var nextPeriodNumber = existingPeriods.Count > 0 ? existingPeriods[^1].PeriodNumber + 1 : 1; var cursor = existingPeriods.Count > 0 ? existingPeriods[^1].EndDate.AddDays(1) : fiscalYear.StartDate; var periods = new List(); for (var i = 0; i < periodCount; i++) { var periodStart = cursor; var periodEnd = periodStart.AddMonths(1).AddDays(-1); periods.Add(new AccountingPeriod { FiscalYearId = fiscalYear.FiscalYearId, PeriodNumber = (short)(nextPeriodNumber + i), StartDate = periodStart, EndDate = periodEnd, Status = PeriodStatus.Open, LockGl = false }); cursor = periodEnd.AddDays(1); } if (periods[^1].EndDate > fiscalYear.EndDate) { throw new BadRequestException("Requested period count exceeds the fiscal year's date range"); } return periods; } } }