< Summary

Information
Class: FAU.Logica.Services.DescuentoPersonalService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/DescuentoPersonalService.cs
Line coverage
86%
Covered lines: 197
Uncovered lines: 30
Coverable lines: 227
Total lines: 371
Line coverage: 86.7%
Branch coverage
70%
Covered branches: 62
Total branches: 88
Branch coverage: 70.4%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
CrearAsync()72.72%222291.66%
EditarAsync()75%8891.66%
EliminarAsync()75%44100%
ConfirmarAsync()75%44100%
ListarAsync()0%2040%
ConfirmarBorradoresAsync(...)100%11100%
ValidarImportacionAsync()100%11100%
AplicarImportacionAsync()62.5%171683.33%
ValidarLoteAsync()76.92%312680.95%
LeerLineasAsync()100%44100%
AgregarError(...)100%11100%
get_NumeroLinea()100%11100%
.ctor(...)100%11100%
get_Resultado()100%11100%
get_Filas()100%11100%
get_Periodo()100%11100%

File(s)

/home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/DescuentoPersonalService.cs

#LineLine coverage
 1using FAU.DataAccess;
 2using FAU.DataAccess.Repositories;
 3using FAU.Entidades;
 4using FAU.Logica.DTOs;
 5using FAU.Logica.Importadores;
 6using FAU.Logica.Validadores;
 7using Microsoft.EntityFrameworkCore;
 8using Microsoft.EntityFrameworkCore.Storage;
 9
 10namespace FAU.Logica.Services;
 11
 12public class DescuentoPersonalService : IDescuentoPersonalService
 13{
 14    private const int MaxFilasImportacion = 20000;
 15
 16    private readonly IDescuentoPersonalRepository _repo;
 17    private readonly IPersonalRepository          _personalRepo;
 18    private readonly IPeriodosRepository          _periodosRepo;
 19    private readonly ApplicationDbContext         _context;
 20    private readonly IAuditoriaService            _auditoria;
 21
 2022    public DescuentoPersonalService(
 2023        IDescuentoPersonalRepository repo,
 2024        IPersonalRepository personalRepo,
 2025        IPeriodosRepository periodosRepo,
 2026        ApplicationDbContext context,
 2027        IAuditoriaService auditoria)
 28    {
 2029        _repo         = repo;
 2030        _personalRepo = personalRepo;
 2031        _periodosRepo = periodosRepo;
 2032        _context      = context;
 2033        _auditoria    = auditoria;
 2034    }
 35
 36    public async Task<DescuentoPersonalPeriodo> CrearAsync(
 37        long periodoId, string cedulaTitular, int codigoConcepto, long acreedorId,
 38        decimal importe, string? observaciones, long usuarioId, DateTime? fechaComunicacion = null)
 39    {
 540        if (importe <= 0)
 141            throw new ArgumentException("El importe debe ser mayor a cero.");
 42
 443        if (acreedorId <= 0)
 044            throw new ArgumentException("El acreedor es requerido.");
 45
 446        var relacion = await _personalRepo.GetByCedulaAsync(cedulaTitular)
 447            ?? throw new ArgumentException($"Funcionario {cedulaTitular} no encontrado.");
 48
 449        var periodo = await _periodosRepo.ObtenerPorIdAsync(periodoId)
 450            ?? throw new ArgumentException($"Período {periodoId} no encontrado.");
 51
 452        if (periodo.Estado != "ABIERTA" && periodo.Estado != Periodo.EstadoListoPCalculo)
 153            throw new InvalidOperationException($"El período está en estado {periodo.Estado} y no admite modificaciones.
 54
 355        var catalogoItem = await _context.CatalogoItems
 356            .FirstOrDefaultAsync(c => c.Codigo == codigoConcepto && c.Vigente)
 357            ?? throw new ArgumentException($"Concepto con código {codigoConcepto} no encontrado o no vigente.");
 58
 359        if (catalogoItem.Tipo != "descuento_personal")
 160            throw new InvalidOperationException($"El concepto {codigoConcepto} (tipo '{catalogoItem.Tipo}') no es un des
 61
 262        if (catalogoItem.ItemParFictoId.HasValue)
 063            throw new InvalidOperationException(
 064                "Los fictos se gestionan en el padrón permanente mediante /api/fictos.");
 65
 266        var acreedor = await _context.Acreedores
 267            .AsNoTracking()
 268            .FirstOrDefaultAsync(a => a.Id == acreedorId && a.Activo)
 269            ?? throw new ArgumentException($"Acreedor {acreedorId} no encontrado o no activo.");
 70
 171        var descuento = new DescuentoPersonalPeriodo
 172        {
 173            PeriodoId         = periodoId,
 174            PersonaId         = relacion.PersonaId,
 175            AcreedorId        = acreedor.Id,
 176            CatalogoItemId    = catalogoItem.Id,
 177            Importe           = importe,
 178            Estado            = "borrador",
 179            Observaciones     = observaciones,
 180            FechaComunicacion = fechaComunicacion ?? DateTime.Now,
 181            UsuarioCreacionId = usuarioId
 182        };
 83
 184        return await _repo.CreateAsync(descuento);
 185    }
 86
 87    public async Task<DescuentoPersonalPeriodo> EditarAsync(long id, decimal importe, string? observaciones, DateTime? f
 88    {
 289        if (importe <= 0)
 090            throw new ArgumentException("El importe debe ser mayor a cero.");
 91
 292        var d = await _repo.GetByIdAsync(id)
 293            ?? throw new ArgumentException($"Descuento {id} no encontrado.");
 94
 295        if (d.Estado != "borrador")
 196            throw new InvalidOperationException("Solo se pueden editar descuentos en estado borrador.");
 97
 198        d.Importe      = importe;
 199        d.Observaciones = observaciones;
 1100        if (fechaComunicacion.HasValue)
 1101            d.FechaComunicacion = fechaComunicacion.Value;
 102
 1103        return await _repo.UpdateAsync(d);
 1104    }
 105
 106    public async Task EliminarAsync(long id)
 107    {
 2108        var d = await _repo.GetByIdAsync(id)
 2109            ?? throw new ArgumentException($"Descuento {id} no encontrado.");
 110
 2111        if (d.Estado != "borrador")
 1112            throw new InvalidOperationException("Solo se pueden eliminar descuentos en estado borrador.");
 113
 1114        await _repo.DeleteAsync(id);
 1115    }
 116
 117    public async Task<DescuentoPersonalPeriodo> ConfirmarAsync(long id)
 118    {
 2119        var descuento = await _repo.GetByIdAsync(id)
 2120            ?? throw new ArgumentException($"Descuento {id} no encontrado.");
 121
 2122        if (descuento.Estado != "borrador")
 1123            throw new InvalidOperationException("Solo se pueden confirmar descuentos en estado borrador.");
 124
 1125        descuento.Estado = "confirmado";
 1126        return await _repo.UpdateAsync(descuento);
 1127    }
 128
 129    public async Task<IEnumerable<DescuentoPersonalPeriodo>> ListarAsync(
 130        long periodoId, string? cedulaTitular, int? codigoConcepto, string? estado)
 131    {
 0132        long? personaId = null;
 0133        if (!string.IsNullOrEmpty(cedulaTitular))
 134        {
 0135            var relacion = await _personalRepo.GetByCedulaAsync(cedulaTitular);
 0136            personaId = relacion?.PersonaId;
 137        }
 0138        return await _repo.GetByPeriodoAsync(periodoId, personaId, codigoConcepto, estado);
 0139    }
 140
 141    public Task<int> ConfirmarBorradoresAsync(long periodoId) =>
 1142        _repo.ConfirmarBorradoresAsync(periodoId);
 143
 144    public async Task<ResultadoValidacionImportacionDescuentosDto> ValidarImportacionAsync(long acreedorId, Stream archi
 145    {
 3146        var lote = await ValidarLoteAsync(acreedorId, archivo);
 3147        return lote.Resultado;
 3148    }
 149
 150    public async Task<ResultadoImportacionDescuentosDto> AplicarImportacionAsync(
 151        long acreedorId, Stream archivo, long usuarioId, string? host)
 152    {
 5153        IDbContextTransaction? transaccion = null;
 5154        if (_context.Database.IsRelational())
 0155            transaccion = await _context.Database.BeginTransactionAsync();
 156
 157        try
 158        {
 5159            var lote = await ValidarLoteAsync(acreedorId, archivo);
 5160            var resultado = new ResultadoImportacionDescuentosDto
 5161            {
 5162                TotalFilas = lote.Resultado.TotalFilas,
 5163                FilasValidas = lote.Resultado.FilasValidas,
 5164                FilasConError = lote.Resultado.FilasConError,
 5165                Errores = lote.Resultado.Errores,
 5166                Aplicado = false
 5167            };
 168
 5169            if (!lote.Resultado.EsValido || lote.Periodo is null)
 170            {
 2171                if (transaccion is not null)
 0172                    await transaccion.RollbackAsync();
 2173                return resultado;
 174            }
 175
 3176            var reemplazados = await _repo.EliminarBorradoresPorAcreedorAsync(lote.Periodo.Id, acreedorId);
 177
 3178            var ahora = DateTime.Now;
 3179            var usuarioValido = usuarioId > 0 ? usuarioId : (long?)null;
 6180            var nuevos = lote.Filas.Select(fila => new DescuentoPersonalPeriodo
 6181            {
 6182                PeriodoId         = lote.Periodo.Id,
 6183                PersonaId         = fila.PersonaId,
 6184                AcreedorId        = acreedorId,
 6185                CatalogoItemId    = fila.CatalogoItemId,
 6186                Importe           = fila.Importe,
 6187                Estado            = "borrador",
 6188                FechaComunicacion = ahora,
 6189                UsuarioCreacionId = usuarioValido
 6190            }).ToList();
 191
 3192            await _repo.CrearVariosAsync(nuevos);
 193
 3194            await _auditoria.LogAuditoriaAsync(
 3195                usuarioId,
 3196                AccionEnum.ImportarDescuentosPersonales,
 3197                ContextoEnum.Descuentos,
 3198                host,
 3199                nameof(DescuentoPersonalPeriodo),
 3200                null,
 3201                new
 3202                {
 3203                    AcreedorId = acreedorId,
 3204                    PeriodoId = lote.Periodo.Id,
 3205                    resultado.TotalFilas,
 3206                    RegistrosCreados = nuevos.Count,
 3207                    RegistrosReemplazados = reemplazados
 3208                });
 209
 3210            if (transaccion is not null)
 0211                await transaccion.CommitAsync();
 212
 3213            resultado.Aplicado = true;
 3214            resultado.RegistrosCreados = nuevos.Count;
 3215            resultado.RegistrosReemplazados = reemplazados;
 3216            return resultado;
 217        }
 0218        catch
 219        {
 0220            if (transaccion is not null)
 0221                await transaccion.RollbackAsync();
 0222            throw;
 223        }
 224        finally
 225        {
 5226            if (transaccion is not null)
 0227                await transaccion.DisposeAsync();
 228        }
 5229    }
 230
 231    private async Task<LoteImportacion> ValidarLoteAsync(long acreedorId, Stream archivo)
 232    {
 8233        var resultado = new ResultadoValidacionImportacionDescuentosDto();
 234
 8235        var periodo = await _periodosRepo.ObtenerEnCursoAsync();
 8236        if (periodo is null)
 237        {
 0238            AgregarError(resultado, 0, "No existe un período en curso.");
 0239            return new LoteImportacion(resultado, [], null);
 240        }
 241
 8242        if (periodo.Estado != "ABIERTA" && periodo.Estado != Periodo.EstadoListoPCalculo)
 243        {
 1244            AgregarError(resultado, 0, $"El período está en estado {periodo.Estado} y no admite modificaciones.");
 1245            return new LoteImportacion(resultado, [], periodo);
 246        }
 247
 7248        var acreedor = await _context.Acreedores
 7249            .AsNoTracking()
 7250            .FirstOrDefaultAsync(a => a.Id == acreedorId && a.Activo);
 7251        if (acreedor is null)
 252        {
 0253            AgregarError(resultado, 0, $"Acreedor {acreedorId} no encontrado o no activo.");
 0254            return new LoteImportacion(resultado, [], periodo);
 255        }
 256
 7257        var lineas = await LeerLineasAsync(archivo);
 7258        if (lineas.Count == 0)
 259        {
 0260            AgregarError(resultado, 0, "El archivo no contiene filas para importar.");
 0261            return new LoteImportacion(resultado, [], periodo);
 262        }
 263
 7264        if (lineas.Count > MaxFilasImportacion)
 265        {
 0266            AgregarError(resultado, 0, $"La importación no puede superar {MaxFilasImportacion} filas por archivo.");
 0267            return new LoteImportacion(resultado, [], periodo);
 268        }
 269
 7270        resultado.TotalFilas = lineas.Count;
 271
 7272        var filasParseadas = new List<DescuentoPersonalTxtFila>();
 30273        foreach (var (linea, numero) in lineas)
 274        {
 8275            if (!DescuentoPersonalTxtLineParser.TryParse(linea, numero, out var fila, out var error))
 276            {
 0277                AgregarError(resultado, numero, error!);
 0278                continue;
 279            }
 8280            filasParseadas.Add(fila!);
 281        }
 282
 15283        var codigos = filasParseadas.Select(f => f.CodigoConcepto).Distinct().ToList();
 7284        var catalogos = await _context.CatalogoItems
 7285            .AsNoTracking()
 7286            .Where(c => codigos.Contains(c.Codigo) && c.Tipo == "descuento_personal" && c.Vigente)
 12287            .ToDictionaryAsync(c => c.Codigo);
 288
 7289        var cedulasCompletas = filasParseadas
 8290            .Select(f => CedulaValidator.CompletarCedula(f.CedulaSinDigito))
 7291            .Distinct()
 7292            .ToList();
 7293        var relaciones = await _context.RelacionesLaborales
 7294            .AsNoTracking()
 7295            .Include(r => r.Persona)
 7296            .Where(r => r.Estado == EstadoRelacion.Activo && cedulasCompletas.Contains(r.Persona.Cedula))
 7297            .ToListAsync();
 7298        var personaIdPorCedula = relaciones
 7299            .GroupBy(r => r.Persona.Cedula)
 21300            .ToDictionary(g => g.Key, g => g.First().PersonaId);
 301
 7302        var claves = new HashSet<(string Cedula, int Codigo)>();
 7303        var filasResueltas = new List<FilaResuelta>();
 304
 30305        foreach (var fila in filasParseadas)
 306        {
 8307            var cedulaCompleta = CedulaValidator.CompletarCedula(fila.CedulaSinDigito);
 308
 8309            if (!catalogos.TryGetValue(fila.CodigoConcepto, out var catalogo))
 310            {
 2311                AgregarError(resultado, fila.NumeroLinea,
 2312                    $"El código de descuento {fila.CodigoConcepto} no está catalogado como descuento personal vigente.")
 2313                continue;
 314            }
 315
 6316            if (catalogo.ItemParFictoId.HasValue)
 317            {
 1318                AgregarError(resultado, fila.NumeroLinea,
 1319                    $"El código {fila.CodigoConcepto} corresponde a un ficto; se gestiona mediante /api/fictos.");
 1320                continue;
 321            }
 322
 5323            if (!personaIdPorCedula.TryGetValue(cedulaCompleta, out var personaId))
 324            {
 0325                AgregarError(resultado, fila.NumeroLinea, $"No se encontró personal activo con cédula {cedulaCompleta}."
 0326                continue;
 327            }
 328
 5329            if (!claves.Add((cedulaCompleta, fila.CodigoConcepto)))
 330            {
 1331                AgregarError(resultado, fila.NumeroLinea,
 1332                    $"Fila duplicada: la cédula {cedulaCompleta} ya aparece con el código {fila.CodigoConcepto} en el ar
 1333                continue;
 334            }
 335
 4336            filasResueltas.Add(new FilaResuelta(fila.NumeroLinea, personaId, catalogo.Id, fila.Importe));
 337        }
 338
 7339        resultado.FilasValidas = filasResueltas.Count;
 7340        return new LoteImportacion(resultado, filasResueltas, periodo);
 8341    }
 342
 343    private static async Task<List<(string Linea, int Numero)>> LeerLineasAsync(Stream archivo)
 344    {
 7345        var lineas = new List<(string, int)>();
 7346        using var lector = new StreamReader(archivo);
 7347        var numero = 0;
 348        string? linea;
 15349        while ((linea = await lector.ReadLineAsync()) != null)
 350        {
 8351            numero++;
 8352            if (string.IsNullOrWhiteSpace(linea))
 353                continue;
 8354            lineas.Add((linea, numero));
 355        }
 7356        return lineas;
 7357    }
 358
 359    private static void AgregarError(ResultadoValidacionImportacionDescuentosDto resultado, int numeroLinea, string mens
 360    {
 5361        resultado.Errores.Add(new ErrorImportacionDescuentoPersonalDto { NumeroLinea = numeroLinea, Mensaje = mensaje })
 5362        resultado.FilasConError++;
 5363    }
 364
 13365    private sealed record FilaResuelta(int NumeroLinea, long PersonaId, long CatalogoItemId, decimal Importe);
 366
 8367    private sealed record LoteImportacion(
 28368        ResultadoValidacionImportacionDescuentosDto Resultado,
 3369        IReadOnlyList<FilaResuelta> Filas,
 20370        Periodo? Periodo);
 371}