feat: add General Ledger integration and financial reporting features
- Implemented General Ledger client component for managing GL integration. - Created a dedicated page for General Ledger configuration. - Developed Finance Report client component for generating various financial reports. - Added report blocks for structured display of financial data. - Introduced API endpoints for fetching GL accounts and financial reports. - Updated navigation to include links for GL integration and financial reports. - Enhanced permissions to manage GL and view financial reports. - Updated appsettings to include General Ledger API configuration.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using Distribution.Data;
|
||||
using Distribution.Infra.Authorization;
|
||||
using Distribution.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Distribution.Features.GeneralLedger;
|
||||
|
||||
public record GlSetupRequest(int? Year);
|
||||
|
||||
public record YearEndCloseRequest(int Year);
|
||||
|
||||
public record OpeningBalanceRequest(DateOnly Date, string? Narration, List<OpeningBalanceLine> Lines);
|
||||
|
||||
[ApiController]
|
||||
[Route("api/gl")]
|
||||
public class GlController(
|
||||
GlSetupService setup,
|
||||
GlPostingService posting,
|
||||
IGeneralLedgerClient gl,
|
||||
Distribution.Features.SysConfig.ISysConfigRepository configRepo,
|
||||
AppDBContext context,
|
||||
ICurrentUserService currentUser) : ControllerBase
|
||||
{
|
||||
[HttpGet("status")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Status()
|
||||
{
|
||||
var mapping = new Dictionary<string, string?>();
|
||||
foreach (var key in GlKeys.AllAccountKeys)
|
||||
mapping[key] = (await configRepo.GetByKeyAsync(key))?.Value;
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
glReachable = await gl.IsHealthyAsync(),
|
||||
setupDone = (await configRepo.GetByKeyAsync(GlKeys.SetupDone))?.Value == "true",
|
||||
openingBalancePosted = await context.GlPostingLogs.AnyAsync(l => l.Event == "OpeningBalance" && l.Status == GlPostingStatus.Posted),
|
||||
closedYears = await posting.GetClosedYearsAsync(),
|
||||
mapping,
|
||||
posted = await context.GlPostingLogs.CountAsync(l => l.Status == GlPostingStatus.Posted),
|
||||
failed = await context.GlPostingLogs.CountAsync(l => l.Status != GlPostingStatus.Posted)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("setup")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Setup(GlSetupRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await setup.RunAsync(request.Year, currentUser.UserId, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("opening-balances")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> OpeningBalances(OpeningBalanceRequest request, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var row = await posting.PostOpeningBalanceAsync(request.Date, request.Narration, request.Lines, ct);
|
||||
return row.Status == GlPostingStatus.Posted
|
||||
? Ok(new { status = row.Status, glJournalNo = row.GlJournalNo })
|
||||
: StatusCode(502, new { error = row.Error ?? "The General Ledger service did not accept the entry. It was saved and can be retried." });
|
||||
}
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("year-end/preview")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> YearEndPreview([FromQuery] int year, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await posting.GetYearEndPreviewAsync(year, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpPost("year-end/close")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> YearEndClose(YearEndCloseRequest request, CancellationToken ct)
|
||||
{
|
||||
try { return Ok(await posting.CloseYearAsync(request.Year, ct)); }
|
||||
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
|
||||
}
|
||||
|
||||
[HttpGet("postings")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Postings([FromQuery] GlPostingStatus? status, [FromQuery] int take = 100)
|
||||
{
|
||||
var query = context.GlPostingLogs.AsQueryable();
|
||||
if (status is not null) query = query.Where(l => l.Status == status);
|
||||
var rows = await query.OrderByDescending(l => l.CreatedAt).Take(Math.Clamp(take, 1, 500))
|
||||
.Select(l => new { l.Id, l.Event, l.SourceId, l.Reference, l.Status, l.GlJournalNo, l.Error, l.Attempts, l.LastAttemptAt, l.CreatedAt })
|
||||
.ToListAsync();
|
||||
return Ok(rows);
|
||||
}
|
||||
|
||||
[HttpPost("postings/retry")]
|
||||
[Authorize("gl.manage")]
|
||||
public async Task<IActionResult> Retry(CancellationToken ct) =>
|
||||
Ok(new { posted = await posting.RetryFailedAsync(ct) });
|
||||
}
|
||||
Reference in New Issue
Block a user