feat(contables): cierre mensual de cuenta corriente de distribuidor
Permite congelar el saldo de un distribuidor por empresa a una fecha de corte y bloquear modificaciones retroactivas sobre el período cerrado. El saldo se calcula sumando movimientos en rango (sin tocar cue_Saldos). Incluye reapertura controlada exclusivamente por SuperAdmin, reporte con saldo inicial, atajo "Desde último cierre", y auditoría del ciclo de vida _H. Permisos CC001/CC002/CC003. Middleware global mapea bloqueos por período cerrado a HTTP 409.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
using GestionIntegral.Api.Data.Repositories.Contables;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace GestionIntegral.Api.Services.Contables
|
||||
{
|
||||
public class PeriodoCerradoValidator : IPeriodoCerradoValidator
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly ILogger<PeriodoCerradoValidator> _logger;
|
||||
|
||||
// TTL del cache: ventana stale aceptada por contrato (los cierres son operación rara).
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
|
||||
|
||||
// Singleton consumiendo Scoped (ICierreCuentaCorrienteRepository) → resolver vía IServiceScopeFactory cada lookup.
|
||||
public PeriodoCerradoValidator(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IMemoryCache cache,
|
||||
ILogger<PeriodoCerradoValidator> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PeriodoCerradoResult> EstaCerradoAsync(string destino, int idDestino, int idEmpresa, DateTime fechaOperacion)
|
||||
{
|
||||
// Sólo aplica a Distribuidores. Canillas no tiene cierre por (Canilla + Empresa).
|
||||
if (!string.Equals(destino, "Distribuidores", StringComparison.OrdinalIgnoreCase))
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
|
||||
var key = CacheKey(idDestino, idEmpresa);
|
||||
|
||||
var ultimo = await _cache.GetOrCreateAsync(key, async entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = CacheTtl;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var repo = scope.ServiceProvider.GetRequiredService<ICierreCuentaCorrienteRepository>();
|
||||
var cierre = await repo.GetUltimoCierreVigenteAsync(idDestino, idEmpresa);
|
||||
|
||||
// Cacheamos sólo lo necesario para que la decisión sea rápida.
|
||||
return cierre == null ? null : new CachedUltimoCierre(cierre.IdCierre, cierre.FechaCorte);
|
||||
});
|
||||
|
||||
if (ultimo == null)
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
|
||||
// FechaCorte es DATE; comparar en .Date para ignorar componente hora.
|
||||
if (ultimo.FechaCorte.Date >= fechaOperacion.Date)
|
||||
return new PeriodoCerradoResult(true, ultimo.IdCierre, ultimo.FechaCorte);
|
||||
|
||||
return new PeriodoCerradoResult(false, null, null);
|
||||
}
|
||||
|
||||
public void InvalidarCache(int idDistribuidor, int idEmpresa)
|
||||
{
|
||||
_cache.Remove(CacheKey(idDistribuidor, idEmpresa));
|
||||
}
|
||||
|
||||
private static string CacheKey(int idDestino, int idEmpresa) => $"cierre-ultimo:{idDestino}:{idEmpresa}";
|
||||
|
||||
private sealed record CachedUltimoCierre(int IdCierre, DateTime FechaCorte);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user