< Summary

Information
Class: FAU.Logica.Services.NovedadPeriodoService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/NovedadPeriodoService.cs
Line coverage
51%
Covered lines: 128
Uncovered lines: 122
Coverable lines: 250
Total lines: 421
Line coverage: 51.2%
Branch coverage
32%
Covered branches: 42
Total branches: 128
Branch coverage: 32.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GetByIdAsync()100%210%
GetAllAsync()100%210%
GetPaginatedAsync()0%4260%
CreateAsync()70.45%484487.34%
UpdateAsync()0%2352480%
ChangeStateToConfirmedAsync()50%4489.47%
AnularAsync()75%4494.11%
DeleteAsync()0%2040%
ValidarFechasDentroDelPeriodo(...)75%8885.71%

File(s)

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

#LineLine coverage
 1using FAU.DataAccess.Repositories;
 2using FAU.Entidades;
 3using FAU.Logica.DTOs;
 4using Microsoft.Extensions.Logging;
 5
 6namespace FAU.Logica.Services;
 7
 8public class NovedadPeriodoService : INovedadPeriodoService
 9{
 110    private static readonly IReadOnlySet<string> TiposPermitidos = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 111    {
 112        "dias_eximidos",
 113        "retiro_parcial",
 114        "subsidio_enfermedad"
 115    };
 16
 17    private readonly INovedadPeriodoRepository _novedadPeriodoRepository;
 18    private readonly IPeriodosRepository _periodosRepository;
 19    private readonly IPersonalRepository _personalRepository;
 20    private readonly IAuditoriaService _auditoriaService;
 21    private readonly ICurrentUserContext _currentUser;
 22    private readonly ILogger<NovedadPeriodoService> _logger;
 23
 1224    public NovedadPeriodoService(
 1225        INovedadPeriodoRepository novedadPeriodoRepository,
 1226        IPeriodosRepository periodosRepository,
 1227        IPersonalRepository personalRepository,
 1228        IAuditoriaService auditoriaService,
 1229        ICurrentUserContext currentUser,
 1230        ILogger<NovedadPeriodoService> logger)
 31    {
 1232        _novedadPeriodoRepository = novedadPeriodoRepository;
 1233        _periodosRepository = periodosRepository;
 1234        _personalRepository = personalRepository;
 1235        _auditoriaService = auditoriaService;
 1236        _currentUser = currentUser;
 1237        _logger = logger;
 1238    }
 39
 40    public async Task<NovedadPeriodo?> GetByIdAsync(long id)
 41    {
 042        return await _novedadPeriodoRepository.GetByIdAsync(id);
 043    }
 44
 45    public async Task<IEnumerable<NovedadPeriodo>> GetAllAsync()
 46    {
 047        return await _novedadPeriodoRepository.GetAllAsync();
 048    }
 49
 50    public async Task<PagedResult<NovedadPeriodoDto>> GetPaginatedAsync(
 51        int page,
 52        int pageSize,
 53        long? periodoId = null,
 54        long? personaId = null,
 55        string? tipo = null,
 56        string? estado = null)
 57    {
 058        if (page < 1) page = 1;
 059        if (pageSize < 1) pageSize = 10;
 060        if (pageSize > 100) pageSize = 100;
 61
 062        var (items, totalCount) = await _novedadPeriodoRepository.GetPaginatedAsync(page, pageSize, periodoId, personaId
 63
 064        var dtos = items.Select(n => new NovedadPeriodoDto
 065        {
 066            Id = n.Id,
 067            PeriodoId = n.PeriodoId,
 068            PersonaId = n.PersonaId,
 069            Cedula = n.Persona?.Cedula ?? string.Empty,
 070            NombreCompleto = n.Persona?.NombreCompleto ?? string.Empty,
 071            Tipo = n.Tipo,
 072            DiasAfectados = n.DiasAfectados,
 073            PorcentajeSubsidio = n.PorcentajeSubsidio,
 074            FechaDesde = n.FechaDesde ?? DateTime.MinValue,
 075            FechaHasta = n.FechaHasta,
 076            Observaciones = n.Observaciones,
 077            Estado = n.Estado,
 078            CreadoPor = n.CreadoPor,
 079            CreadoEn = n.CreadoEn,
 080            ConfirmadoPor = n.ConfirmadoPor,
 081            ConfirmadoEn = n.ConfirmadoEn
 082        })
 083        .ToList();
 84
 085        return new PagedResult<NovedadPeriodoDto>
 086        {
 087            Items = dtos,
 088            Page = page,
 089            PageSize = pageSize,
 090            TotalCount = totalCount
 091        };
 092    }
 93
 94    public async Task<(bool Success, NovedadPeriodo? Result, string? Error)> CreateAsync(CrearNovedadPeriodoRequest requ
 95    {
 996        if (request == null)
 97        {
 098            return (false, null, "La solicitud de creación es obligatoria");
 99        }
 100
 9101        if (request.PeriodoId <= 0)
 102        {
 0103            return (false, null, "El PeriodoId es requerido");
 104        }
 105
 9106        if (string.IsNullOrWhiteSpace(request.Cedula))
 107        {
 0108            return (false, null, "La cédula del funcionario es requerida");
 109        }
 110
 9111        if (string.IsNullOrWhiteSpace(request.Tipo))
 112        {
 0113            return (false, null, "El tipo de novedad es requerido");
 114        }
 115
 9116        if (!TiposPermitidos.Contains(request.Tipo.Trim()))
 117        {
 0118            return (false, null, "El tipo de novedad no es válido");
 119        }
 120
 9121        if (request.Tipo.Trim() == "dias_eximidos" && !request.PorcentajeSubsidio.HasValue)
 122        {
 1123            return (false, null, "El porcentaje de subsidio es requerido para días eximidos");
 124        }
 125
 8126        if (request.PorcentajeSubsidio.HasValue && (request.PorcentajeSubsidio < 0 || request.PorcentajeSubsidio > 100))
 127        {
 0128            return (false, null, "El porcentaje de subsidio debe estar entre 0 y 100");
 129        }
 130
 8131        var periodo = await _periodosRepository.ObtenerPorIdAsync(request.PeriodoId);
 8132        if (periodo == null)
 133        {
 1134            return (false, null, "Periodo no encontrado");
 135        }
 136
 137        DateTime fechaDesde;
 138        DateTime fechaHasta;
 139        short diasAfectados;
 140
 7141        if (request.MesCompleto)
 142        {
 143            // Mes completo: min(30, días reales del mes del período)
 144            // Febrero = 28 o 29; meses de 31 días = 30 (tope del sistema)
 2145            var diasRealesMes = DateTime.DaysInMonth(periodo.Anio, periodo.Mes);
 2146            var diasMes = (short)Math.Min(30, diasRealesMes);
 2147            fechaDesde = new DateTime(periodo.Anio, periodo.Mes, 1);
 2148            fechaHasta = new DateTime(periodo.Anio, periodo.Mes, diasMes);
 2149            diasAfectados = diasMes;
 150        }
 151        else
 152        {
 5153            if (request.FechaDesde is null || request.FechaDesde == default)
 0154                return (false, null, "La fecha desde es requerida");
 155
 5156            if (request.FechaHasta is null || request.FechaHasta == default)
 0157                return (false, null, "La fecha hasta es requerida");
 158
 5159            fechaDesde = request.FechaDesde.Value.Date;
 5160            fechaHasta = request.FechaHasta.Value.Date;
 161
 5162            if (fechaHasta < fechaDesde)
 1163                return (false, null, "La fecha hasta no puede ser anterior a la fecha desde");
 164
 4165            var errorFechas = ValidarFechasDentroDelPeriodo(fechaDesde, fechaHasta, periodo);
 4166            if (errorFechas is not null)
 1167                return (false, null, errorFechas);
 168
 3169            diasAfectados = (short)((fechaHasta - fechaDesde).Days + 1);
 3170            if (diasAfectados > 30)
 1171                return (false, null, "El sistema calcula haberes sobre 30 días por mes. El rango no puede superar 30 día
 172        }
 173
 4174        var relacion = await _personalRepository.GetByCedulaAsync(request.Cedula.Trim());
 4175        if (relacion == null)
 176        {
 0177            return (false, null, $"No se encontró un funcionario activo con cédula {request.Cedula}");
 178        }
 179
 4180        if (await _novedadPeriodoRepository.ExistsAsync(request.PeriodoId, relacion.PersonaId, request.Tipo, fechaDesde)
 181        {
 0182            return (false, null, "Ya existe una novedad de período con el mismo tipo para esta persona y período");
 183        }
 184
 4185        var entidad = new NovedadPeriodo
 4186        {
 4187            PeriodoId = request.PeriodoId,
 4188            PersonaId = relacion.PersonaId,
 4189            Tipo = request.Tipo.Trim(),
 4190            DiasAfectados = diasAfectados,
 4191            PorcentajeSubsidio = request.PorcentajeSubsidio,
 4192            FechaDesde = fechaDesde,
 4193            FechaHasta = fechaHasta,
 4194            Observaciones = string.IsNullOrWhiteSpace(request.Observaciones) ? null : request.Observaciones.Trim(),
 4195            Estado = "borrador",
 4196            CreadoPor = _currentUser.UserId,
 4197            CreadoEn = DateTime.Now
 4198        };
 199
 4200        var creado = await _novedadPeriodoRepository.CreateAsync(entidad);
 201
 4202        await _auditoriaService.LogAuditoriaAsync(
 4203            usuarioId: _currentUser.UserId ?? 0,
 4204            AccionEnum.CrearNovedad,
 4205            ContextoEnum.Novedades,
 4206            host: _currentUser.Host,
 4207            entidad: nameof(NovedadPeriodo),
 4208            entidadId: creado.Id,
 4209            detalle: new
 4210            {
 4211                operacion = "crear",
 4212                periodoId = creado.PeriodoId,
 4213                personaId = creado.PersonaId,
 4214                tipo = creado.Tipo,
 4215                diasAfectados = creado.DiasAfectados,
 4216                porcentajeSubsidio = creado.PorcentajeSubsidio,
 4217                fechaDesde = creado.FechaDesde,
 4218                fechaHasta = creado.FechaHasta,
 4219                mesCompleto = request.MesCompleto,
 4220                estado = creado.Estado
 4221            });
 222
 4223        return (true, creado, null);
 9224    }
 225
 226    public async Task<(bool Success, NovedadPeriodo? Result, string? Error)> UpdateAsync(long id, ActualizarNovedadPerio
 227    {
 0228        if (request == null)
 229        {
 0230            return (false, null, "La solicitud de actualización es obligatoria");
 231        }
 232
 0233        var novedad = await _novedadPeriodoRepository.GetByIdAsync(id);
 0234        if (novedad == null)
 235        {
 0236            return (false, null, "Novedad de período no encontrada");
 237        }
 238
 0239        if (novedad.Estado != "borrador")
 240        {
 0241            return (false, null, "Solo se pueden actualizar novedades en estado borrador");
 242        }
 243
 0244        var cambiaFechas = request.FechaDesde.HasValue || request.FechaHasta.HasValue;
 245
 0246        if (!cambiaFechas && !request.PorcentajeSubsidio.HasValue && request.Observaciones is null)
 247        {
 0248            return (false, null, "Debe proporcionar al menos un campo para actualizar");
 249        }
 250
 0251        if (request.PorcentajeSubsidio.HasValue && (request.PorcentajeSubsidio < 0 || request.PorcentajeSubsidio > 100))
 252        {
 0253            return (false, null, "El porcentaje de subsidio debe estar entre 0 y 100");
 254        }
 255
 0256        var fechaDesde = request.FechaDesde?.Date ?? novedad.FechaDesde?.Date ?? default;
 0257        var fechaHasta = request.FechaHasta?.Date ?? novedad.FechaHasta?.Date ?? default;
 258
 0259        if (cambiaFechas)
 260        {
 0261            if (fechaDesde == default)
 0262                return (false, null, "La fecha desde es requerida");
 263
 0264            if (fechaHasta == default)
 0265                return (false, null, "La fecha hasta es requerida");
 266
 0267            if (fechaHasta < fechaDesde)
 0268                return (false, null, "La fecha hasta no puede ser anterior a la fecha desde");
 269
 0270            var periodo = await _periodosRepository.ObtenerPorIdAsync(novedad.PeriodoId);
 0271            if (periodo is not null)
 272            {
 0273                var errorFechas = ValidarFechasDentroDelPeriodo(fechaDesde, fechaHasta, periodo);
 0274                if (errorFechas is not null)
 0275                    return (false, null, errorFechas);
 276            }
 277
 0278            var diasNuevos = (short)((fechaHasta - fechaDesde).Days + 1);
 0279            if (diasNuevos > 30)
 0280                return (false, null, "El sistema calcula haberes sobre 30 días por mes. El rango no puede superar 30 día
 281
 0282            novedad.FechaDesde = fechaDesde;
 0283            novedad.FechaHasta = fechaHasta;
 0284            novedad.DiasAfectados = diasNuevos;
 285        }
 286
 0287        var antes = new
 0288        {
 0289            id = novedad.Id,
 0290            fechaDesde = novedad.FechaDesde,
 0291            fechaHasta = novedad.FechaHasta,
 0292            diasAfectados = novedad.DiasAfectados,
 0293            porcentajeSubsidio = novedad.PorcentajeSubsidio,
 0294            observaciones = novedad.Observaciones
 0295        };
 296
 0297        if (request.PorcentajeSubsidio.HasValue)
 298        {
 0299            novedad.PorcentajeSubsidio = request.PorcentajeSubsidio;
 300        }
 301
 0302        if (request.Observaciones is not null)
 303        {
 0304            novedad.Observaciones = string.IsNullOrWhiteSpace(request.Observaciones) ? null : request.Observaciones.Trim
 305        }
 306
 0307        var actualizado = await _novedadPeriodoRepository.UpdateAsync(novedad);
 308
 0309        await _auditoriaService.LogAuditoriaAsync(
 0310            usuarioId: _currentUser.UserId ?? 0,
 0311            AccionEnum.ActualizarBanco,
 0312            ContextoEnum.Novedades,
 0313            host: _currentUser.Host,
 0314            entidad: nameof(NovedadPeriodo),
 0315            entidadId: actualizado.Id,
 0316            detalle: new { operacion = "actualizar", antes, despues = new { id = actualizado.Id, fechaDesde = actualizad
 317
 0318        return (true, actualizado, null);
 0319    }
 320
 321    public async Task<(bool Success, NovedadPeriodo? Result, string? Error)> ChangeStateToConfirmedAsync(long id)
 322    {
 1323        var novedad = await _novedadPeriodoRepository.GetByIdAsync(id);
 1324        if (novedad == null)
 325        {
 0326            return (false, null, "Novedad de período no encontrada");
 327        }
 328
 1329        if (novedad.Estado != "borrador")
 330        {
 0331            return (false, null, "Solo se puede confirmar una novedad en estado borrador");
 332        }
 333
 1334        novedad.Estado = "confirmada";
 1335        novedad.ConfirmadoPor = _currentUser.UserId;
 1336        novedad.ConfirmadoEn = DateTime.Now;
 337
 1338        var actualizado = await _novedadPeriodoRepository.UpdateAsync(novedad);
 339
 1340        await _auditoriaService.LogAuditoriaAsync(
 1341            usuarioId: _currentUser.UserId ?? 0,
 1342            AccionEnum.ConfirmarNovedad,
 1343            ContextoEnum.Novedades,
 1344            host: _currentUser.Host,
 1345            entidad: nameof(NovedadPeriodo),
 1346            entidadId: actualizado.Id,
 1347            detalle: new { operacion = "confirmar", estado = actualizado.Estado });
 348
 1349        return (true, actualizado, null);
 1350    }
 351
 352    public async Task<(bool Success, NovedadPeriodo? Result, string? Error)> AnularAsync(long id)
 353    {
 2354        var novedad = await _novedadPeriodoRepository.GetByIdAsync(id);
 2355        if (novedad == null)
 356        {
 0357            return (false, null, "Novedad de período no encontrada");
 358        }
 359
 2360        if (novedad.Estado != "confirmada")
 361        {
 1362            return (false, null, "Solo se puede anular una novedad en estado confirmada");
 363        }
 364
 1365        novedad.Estado = "anulada";
 366
 1367        var actualizado = await _novedadPeriodoRepository.UpdateAsync(novedad);
 368
 1369        await _auditoriaService.LogAuditoriaAsync(
 1370            usuarioId: _currentUser.UserId ?? 0,
 1371            AccionEnum.AnularNovedad,
 1372            ContextoEnum.Novedades,
 1373            host: _currentUser.Host,
 1374            entidad: nameof(NovedadPeriodo),
 1375            entidadId: actualizado.Id,
 1376            detalle: new { operacion = "anular", estado = actualizado.Estado });
 377
 1378        return (true, actualizado, null);
 2379    }
 380
 381    public async Task<(bool Success, NovedadPeriodo? Result, string? Error)> DeleteAsync(long id)
 382    {
 0383        var novedad = await _novedadPeriodoRepository.GetByIdAsync(id);
 0384        if (novedad == null)
 385        {
 0386            return (false, null, "Novedad de período no encontrada");
 387        }
 388
 0389        if (novedad.Estado != "borrador")
 390        {
 0391            return (false, null, "Solo se pueden eliminar novedades en estado borrador");
 392        }
 393
 0394        await _novedadPeriodoRepository.DeleteAsync(id);
 395
 0396        await _auditoriaService.LogAuditoriaAsync(
 0397            usuarioId: _currentUser.UserId ?? 0,
 0398            AccionEnum.AnularNovedad,
 0399            ContextoEnum.Novedades,
 0400            host: _currentUser.Host,
 0401            entidad: nameof(NovedadPeriodo),
 0402            entidadId: id,
 0403            detalle: new { operacion = "eliminar" });
 404
 0405        return (true, novedad, null);
 0406    }
 407
 408    private static string? ValidarFechasDentroDelPeriodo(DateTime fechaDesde, DateTime fechaHasta, Periodo periodo)
 409    {
 4410        var primerDia = new DateTime(periodo.Anio, periodo.Mes, 1);
 4411        var ultimoDia = primerDia.AddMonths(1).AddDays(-1);
 412
 4413        if (fechaDesde < primerDia || fechaDesde > ultimoDia)
 1414            return $"La fecha desde debe estar dentro del período {periodo.Anio}/{periodo.Mes:D2}";
 415
 3416        if (fechaHasta < primerDia || fechaHasta > ultimoDia)
 0417            return $"La fecha hasta debe estar dentro del período {periodo.Anio}/{periodo.Mes:D2}";
 418
 3419        return null;
 420    }
 421}