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 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 Status() { var mapping = new Dictionary(); 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 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 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 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 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 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 Retry(CancellationToken ct) => Ok(new { posted = await posting.RetryFailedAsync(ct) }); }