Distribution_GL Initialization
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
using General_Ledger_Service.DTOs.Common;
|
||||
using General_Ledger_Service.DTOs.JournalEntries;
|
||||
using General_Ledger_Service.Enums;
|
||||
using General_Ledger_Service.Exceptions;
|
||||
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
|
||||
{
|
||||
// Retrofit - the client always sends a complete, ready-to-post journal entry in one call;
|
||||
// there is no client-facing DRAFT state anymore. CreateAsync validates, inserts, balance-checks,
|
||||
// period-lock-checks, and posts, all in one transaction. lines is required for a plain create
|
||||
// and for a PartialAdjustment correction (previously deferred to a separate SetLines call);
|
||||
// FullReversal still auto-builds its lines by mirroring the original.
|
||||
public class JournalEntryService(
|
||||
IJournalEntryRepository journalEntryRepository,
|
||||
IAccountRepository accountRepository,
|
||||
ICostCenterRepository costCenterRepository,
|
||||
IFiscalCalendarRepository fiscalCalendarRepository,
|
||||
IAuditLogRepository auditLogRepository,
|
||||
IUnitOfWork unitOfWork) : IJournalEntryService
|
||||
{
|
||||
private readonly IJournalEntryRepository _journalEntryRepository = journalEntryRepository;
|
||||
private readonly IAccountRepository _accountRepository = accountRepository;
|
||||
private readonly ICostCenterRepository _costCenterRepository = costCenterRepository;
|
||||
private readonly IFiscalCalendarRepository _fiscalCalendarRepository = fiscalCalendarRepository;
|
||||
private readonly IAuditLogRepository _auditLogRepository = auditLogRepository;
|
||||
private readonly IUnitOfWork _unitOfWork = unitOfWork;
|
||||
|
||||
public async Task<ApiResponse> CreateAsync(CreateJournalEntryRequest request)
|
||||
{
|
||||
var period = await _fiscalCalendarRepository.GetPeriodByIdAsync(request.PeriodId)
|
||||
?? throw new NotFoundException("Period not found");
|
||||
|
||||
var entry = new JournalEntry
|
||||
{
|
||||
JournalNo = await GenerateJournalNoAsync(request.EntryDate.Year),
|
||||
PeriodId = request.PeriodId,
|
||||
EntryDate = request.EntryDate,
|
||||
JournalType = request.JournalType,
|
||||
SourceModule = request.SourceModule,
|
||||
Reference = request.Reference,
|
||||
Narration = request.Narration,
|
||||
PreparedBy = request.PreparedBy,
|
||||
IsPosted = false,
|
||||
IsReversed = false,
|
||||
EntryStatus = JournalEntryStatus.Draft
|
||||
};
|
||||
|
||||
List<JournalEntryLineRequest>? linesToBuild = null;
|
||||
List<JournalEntryLine>? mirroredLines = null;
|
||||
JournalEntry? original = null;
|
||||
JournalCorrectionType correctionType = default;
|
||||
var isCorrection = request.Correction != null;
|
||||
var auditAction = AuditAction.Create;
|
||||
|
||||
if (isCorrection)
|
||||
{
|
||||
var correction = request.Correction!;
|
||||
|
||||
if (!Enum.TryParse(correction.CorrectionType, ignoreCase: true, out correctionType))
|
||||
{
|
||||
throw new BadRequestException(
|
||||
$"Invalid correctionType '{correction.CorrectionType}'. Must be one of: {string.Join(", ", Enum.GetNames<JournalCorrectionType>())}.");
|
||||
}
|
||||
|
||||
original = await _journalEntryRepository.GetJournalEntryByJournalNoAsync(correction.OriginalJournalNo)
|
||||
?? throw new NotFoundException("Original journal entry not found");
|
||||
|
||||
if (original.EntryStatus != JournalEntryStatus.Posted)
|
||||
{
|
||||
throw new BadRequestException("Original journal entry must be posted before it can be corrected");
|
||||
}
|
||||
|
||||
if (correctionType == JournalCorrectionType.FullReversal)
|
||||
{
|
||||
mirroredLines = [.. original.Lines.Select(l => new JournalEntryLine
|
||||
{
|
||||
AccountId = l.AccountId,
|
||||
DebitAmount = l.CreditAmount,
|
||||
CreditAmount = l.DebitAmount,
|
||||
CurrencyCode = l.CurrencyCode,
|
||||
ExchangeRate = l.ExchangeRate,
|
||||
BaseDebit = l.BaseCredit,
|
||||
BaseCredit = l.BaseDebit,
|
||||
CostCenterId = l.CostCenterId,
|
||||
Memo = l.Memo
|
||||
})];
|
||||
entry.ReversalOfId = original.JournalId;
|
||||
auditAction = AuditAction.Reverse;
|
||||
}
|
||||
else
|
||||
{
|
||||
// PartialAdjustment: lines are now required inline, same as a plain create -
|
||||
// there's no more follow-up call to supply them afterward.
|
||||
linesToBuild = request.Lines;
|
||||
auditAction = AuditAction.Adjust;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
linesToBuild = request.Lines;
|
||||
}
|
||||
|
||||
if (mirroredLines is null && (linesToBuild is null || linesToBuild.Count == 0))
|
||||
{
|
||||
throw new BadRequestException("lines is required");
|
||||
}
|
||||
|
||||
await _unitOfWork.BeginAsync();
|
||||
try
|
||||
{
|
||||
await _journalEntryRepository.AddJournalEntryAsync(entry);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
List<JournalEntryLine> finalLines;
|
||||
|
||||
if (mirroredLines != null)
|
||||
{
|
||||
foreach (var line in mirroredLines)
|
||||
{
|
||||
line.JournalId = entry.JournalId;
|
||||
}
|
||||
await _journalEntryRepository.AddJournalEntryLinesAsync(mirroredLines);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
finalLines = mirroredLines;
|
||||
}
|
||||
else
|
||||
{
|
||||
var builtLines = new List<JournalEntryLine>();
|
||||
foreach (var lineRequest in linesToBuild!)
|
||||
{
|
||||
var line = await BuildLineAsync(lineRequest);
|
||||
line.JournalId = entry.JournalId;
|
||||
builtLines.Add(line);
|
||||
}
|
||||
await _journalEntryRepository.AddJournalEntryLinesAsync(builtLines);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
finalLines = builtLines;
|
||||
}
|
||||
|
||||
entry.Lines = finalLines;
|
||||
|
||||
if (isCorrection)
|
||||
{
|
||||
var correctionRow = new JournalCorrection
|
||||
{
|
||||
OriginalJournalId = original!.JournalId,
|
||||
CorrectionJournalId = entry.JournalId,
|
||||
CorrectionType = correctionType,
|
||||
Reason = request.Correction!.Reason,
|
||||
CorrectedBy = request.Correction!.CorrectedBy
|
||||
};
|
||||
await _journalEntryRepository.AddJournalCorrectionAsync(correctionRow);
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Ledger,
|
||||
EntityName = "JournalEntry",
|
||||
EntityId = entry.JournalId.ToString(),
|
||||
Action = auditAction,
|
||||
Description = $"Journal entry {entry.JournalNo} created",
|
||||
PerformedBy = request.PreparedBy
|
||||
});
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
// Immediately post - there is no client-facing DRAFT state to leave this in.
|
||||
var totalDebit = finalLines.Sum(l => l.DebitAmount);
|
||||
var totalCredit = finalLines.Sum(l => l.CreditAmount);
|
||||
if (totalDebit != totalCredit)
|
||||
{
|
||||
throw new BadRequestException("Journal entry does not balance");
|
||||
}
|
||||
|
||||
if (period.Status is PeriodStatus.Locked or PeriodStatus.Closed || period.LockGl)
|
||||
{
|
||||
throw new BadRequestException("Period is locked");
|
||||
}
|
||||
|
||||
entry.IsPosted = true;
|
||||
entry.EntryStatus = JournalEntryStatus.Posted;
|
||||
|
||||
if (correctionType == JournalCorrectionType.FullReversal)
|
||||
{
|
||||
original!.EntryStatus = JournalEntryStatus.Reversed;
|
||||
original.IsReversed = true;
|
||||
}
|
||||
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Ledger,
|
||||
EntityName = "JournalEntry",
|
||||
EntityId = entry.JournalId.ToString(),
|
||||
Action = AuditAction.Post,
|
||||
Description = $"Journal entry {entry.JournalNo} posted",
|
||||
PerformedBy = null
|
||||
});
|
||||
|
||||
if (correctionType == JournalCorrectionType.FullReversal)
|
||||
{
|
||||
await _auditLogRepository.AddAsync(new AuditLog
|
||||
{
|
||||
Category = AuditCategory.Ledger,
|
||||
EntityName = "JournalEntry",
|
||||
EntityId = original!.JournalId.ToString(),
|
||||
Action = AuditAction.Reverse,
|
||||
Description = $"Journal entry {original.JournalNo} reversed by {entry.JournalNo}",
|
||||
PerformedBy = null
|
||||
});
|
||||
}
|
||||
await _unitOfWork.SaveChangesAsync();
|
||||
|
||||
await _unitOfWork.CommitAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _unitOfWork.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
|
||||
return ResponseHelpers.Created("Journal entry created and posted successfully", entry);
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> GetByJournalNoAsync(string journalNo)
|
||||
{
|
||||
var entry = await _journalEntryRepository.GetJournalEntryByJournalNoAsync(journalNo)
|
||||
?? throw new NotFoundException("Journal entry not found");
|
||||
|
||||
return ResponseHelpers.Ok("Journal entry retrieved successfully", entry);
|
||||
}
|
||||
|
||||
private async Task<JournalEntryLine> BuildLineAsync(JournalEntryLineRequest request)
|
||||
{
|
||||
var account = await _accountRepository.GetByCodeAsync(request.AccountCode)
|
||||
?? throw new NotFoundException($"Account {request.AccountCode} not found");
|
||||
|
||||
if (!account.IsPostable)
|
||||
{
|
||||
throw new BadRequestException($"Account {account.AccountCode} is not postable");
|
||||
}
|
||||
|
||||
long? costCenterId = null;
|
||||
if (!string.IsNullOrWhiteSpace(request.CostCenterCode))
|
||||
{
|
||||
var costCenter = await _costCenterRepository.GetByCodeAsync(request.CostCenterCode)
|
||||
?? throw new NotFoundException($"Cost center {request.CostCenterCode} not found");
|
||||
costCenterId = costCenter.CostCenterId;
|
||||
}
|
||||
|
||||
return new JournalEntryLine
|
||||
{
|
||||
AccountId = account.AccountId,
|
||||
DebitAmount = request.DebitAmount,
|
||||
CreditAmount = request.CreditAmount,
|
||||
CurrencyCode = account.CurrencyCode,
|
||||
ExchangeRate = 1,
|
||||
BaseDebit = request.DebitAmount,
|
||||
BaseCredit = request.CreditAmount,
|
||||
CostCenterId = costCenterId,
|
||||
Memo = request.Memo
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> GenerateJournalNoAsync(int year)
|
||||
{
|
||||
var count = await _journalEntryRepository.CountJournalEntriesForYearAsync(year);
|
||||
return $"JV-{year}-{(count + 1):D6}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user