ca81e9bc42
- 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.
101 lines
4.1 KiB
C#
101 lines
4.1 KiB
C#
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) });
|
|
}
|