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.
70 lines
2.9 KiB
C#
70 lines
2.9 KiB
C#
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);
|
|
}
|
|
}
|