< Summary

Information
Class: FAU.Logica.Services.BeneficioSocialService
Assembly: FAU.Logica
File(s): /home/runner/work/Sistema.Liquidacion.BE/Sistema.Liquidacion.BE/FAU.Logica/Services/BeneficioSocialService.cs
Line coverage
65%
Covered lines: 224
Uncovered lines: 119
Coverable lines: 343
Total lines: 612
Line coverage: 65.3%
Branch coverage
50%
Covered branches: 158
Total branches: 310
Branch coverage: 50.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using FAU.DataAccess.Repositories;
 2using FAU.Entidades;
 3using FAU.Logica.DTOs;
 4
 5namespace FAU.Logica.Services;
 6
 7public class BeneficioSocialService : IBeneficioSocialService
 8{
 9    private const string EstadoActivo = "activo";
 10    private const string EstadoInactivo = "inactivo";
 11    private const string EstadoPendienteDoc = "pendiente_doc";
 12
 13    private readonly IBeneficioSocialRepository _beneficioSocialRepository;
 14    private readonly IAuditoriaService _auditoriaService;
 15    private readonly IPeriodoGuard _periodoGuard;
 16    private readonly IStorageService _storage;
 17    private readonly IDocumentoRepository _docRepo;
 18
 1019    public BeneficioSocialService(
 1020        IBeneficioSocialRepository beneficioSocialRepository,
 1021        IAuditoriaService auditoriaService,
 1022        IPeriodoGuard periodoGuard,
 1023        IStorageService storage,
 1024        IDocumentoRepository docRepo)
 25    {
 1026        _beneficioSocialRepository = beneficioSocialRepository;
 1027        _auditoriaService = auditoriaService;
 1028        _periodoGuard = periodoGuard;
 1029        _storage = storage;
 1030        _docRepo = docRepo;
 1031    }
 32
 33    public async Task<BeneficioSocialDto?> ObtenerPorIdAsync(long id)
 34    {
 235        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 236        return entidad == null ? null : MapearDto(entidad);
 237    }
 38
 39    public async Task<PagedResult<BeneficioSocialDto>> ObtenerPaginadoAsync(int pagina, int tamano, long? personaId = nu
 40    {
 241        if (pagina < 1) pagina = 1;
 142        if (tamano < 1) tamano = 10;
 243        if (tamano > 100) tamano = 100;
 44
 145        var (items, totalCount) = await _beneficioSocialRepository.ObtenerPaginadoAsync(pagina, tamano, personaId, estad
 46
 147        return new PagedResult<BeneficioSocialDto>
 148        {
 149            Items = items.Select(item => MapearDto(item)).ToList(),
 150            Page = pagina,
 151            PageSize = tamano,
 152            TotalCount = totalCount
 153        };
 154    }
 55
 56    public async Task<(bool Exito, BeneficioSocialDto? Resultado, string? Error)> CrearAsync(CrearBeneficioSocialRequest
 57    {
 358        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 359        var primerDiaPeriodo = await _periodoGuard.ObtenerPrimerDiaPeriodoAbiertoAsync();
 60        try
 61        {
 362            var tipo = await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(request.TipoBeneficioId);
 363            if (tipo == null || !tipo.Activo)
 64            {
 065                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 66            }
 67
 368            var categoriaEsperada = ConvertirCategoriaRequest(DeterminarCategoria(tipo));
 369            if (request.CategoriaBeneficio != categoriaEsperada)
 70            {
 071                return (false, null, "La categoria declarada no coincide con el tipo de beneficio seleccionado");
 72            }
 73
 374            var personaActiva = await _beneficioSocialRepository.ExistePersonaActivaAsync(request.PersonaId, request.Vig
 375            if (!personaActiva)
 76            {
 177                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 78            }
 79
 280            var validacion = ValidarReglasPorTipo(tipo, request, esActualizacion: false);
 281            if (!string.IsNullOrEmpty(validacion))
 82            {
 083                return (false, null, validacion);
 84            }
 85
 86            // Para beneficios mensuales: no permitir más de uno activo del mismo tipo por persona
 287            if (tipo.EsMensual && await _beneficioSocialRepository.ExisteBeneficioActivoMismoTipoAsync(request.PersonaId
 88            {
 089                return (false, null, $"La persona ya tiene un beneficio activo del tipo '{tipo.Nombre}'. Debe darlo de b
 90            }
 91
 292            var claveEvento = ConstruirClaveEvento(tipo, request.DatosTipo.FechaEvento, request.VigenteDesde);
 293            if (EsBeneficioUnicaVez(tipo) && await _beneficioSocialRepository.ExisteDuplicadoEventoAsync(request.Persona
 94            {
 195                return (false, null, "Existe un beneficio duplicado para un evento de unica vez");
 96            }
 97
 198            var retroactividadPendiente = CalcularRetroactividadPendiente(request.VigenteDesde, primerDiaPeriodo);
 199            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 100            {
 0101                return (false, null, "El tipo de beneficio no permite retroactividad para la vigencia indicada");
 102            }
 103
 1104            var estado = request.DatosTipo.PendienteDocumentacion ? EstadoPendienteDoc : EstadoActivo;
 1105            var entidad = new BeneficioSocial
 1106            {
 1107                PersonaId = request.PersonaId,
 1108                TipoBeneficioId = request.TipoBeneficioId,
 1109                Cantidad = (short)Math.Max(request.Cantidad ?? 1, (short)0),
 1110                VigenteDesde = request.VigenteDesde.Date,
 1111                VigenteHasta = request.VigenteHasta?.Date,
 1112                Estado = estado,
 1113                DocumentoRespaldo = request.DocumentoRespaldo?.Trim(),
 1114                Observaciones = ConstruirObservaciones(request.Observaciones, request.DatosTipo.JustificacionRetroactivo
 1115                CreadoPor = usuarioId,
 1116                FechaEvento = request.DatosTipo.FechaEvento?.Date,
 1117                ClaveEvento = claveEvento,
 1118                Subtipo = (request.DatosTipo.Subtipo ?? request.DatosTipo.EstadoCivilUnion)?.Trim().ToUpperInvariant(),
 1119                RetroactividadPendiente = retroactividadPendiente
 1120            };
 121
 1122            var creado = await _beneficioSocialRepository.CrearAsync(entidad);
 123
 1124            await _auditoriaService.LogAuditoriaAsync(
 1125                usuarioId,
 1126                AccionEnum.CrearBeneficio,
 1127                ContextoEnum.Beneficios,
 1128                host: host,
 1129                entidad: nameof(BeneficioSocial),
 1130                entidadId: creado.Id,
 1131                detalle: new { operacion = "crear", id = creado.Id, personaId = creado.PersonaId, tipoBeneficioId = crea
 132
 1133            var recargado = await _beneficioSocialRepository.ObtenerPorIdAsync(creado.Id);
 1134            return (true, recargado == null ? MapearDto(creado, tipo) : MapearDto(recargado), null);
 135        }
 0136        catch (Exception ex)
 137        {
 0138            return (false, null, $"Error al crear beneficio social: {ex.Message}");
 139        }
 3140    }
 141
 142    public async Task<(bool Exito, BeneficioSocialDto? Resultado, string? Error)> ActualizarAsync(long id, ActualizarBen
 143    {
 2144        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 2145        var primerDiaPeriodo = await _periodoGuard.ObtenerPrimerDiaPeriodoAbiertoAsync();
 146        try
 147        {
 2148            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 2149            if (entidad == null)
 150            {
 1151                return (false, null, "Beneficio social no encontrado");
 152            }
 153
 1154            var tipo = entidad.TipoBeneficio ?? await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(entidad.TipoB
 1155            if (tipo == null || !tipo.Activo)
 156            {
 0157                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 158            }
 159
 1160            var vigenteDesde = request.VigenteDesde?.Date ?? entidad.VigenteDesde.Date;
 1161            var fechaEvento = (request.FechaEvento ?? request.DatosTipo?.FechaEvento)?.Date ?? entidad.FechaEvento;
 162
 1163            if (!await _beneficioSocialRepository.ExistePersonaActivaAsync(entidad.PersonaId, vigenteDesde))
 164            {
 0165                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 166            }
 167
 1168            var requestConsolidado = new CrearBeneficioSocialRequest
 1169            {
 1170                PersonaId = entidad.PersonaId,
 1171                TipoBeneficioId = entidad.TipoBeneficioId,
 1172                Cantidad = request.Cantidad ?? entidad.Cantidad,
 1173                VigenteDesde = vigenteDesde,
 1174                VigenteHasta = request.VigenteHasta ?? entidad.VigenteHasta,
 1175                DocumentoRespaldo = request.DocumentoRespaldo ?? entidad.DocumentoRespaldo,
 1176                Observaciones = request.Observaciones ?? entidad.Observaciones,
 1177                CategoriaBeneficio = ConvertirCategoriaRequest(DeterminarCategoria(tipo)),
 1178                DatosTipo = new DatosTipoBeneficioRequest
 1179                {
 1180                    FechaEvento = fechaEvento,
 1181                    EstadoCivilUnion = !string.IsNullOrWhiteSpace(request.ResolverEstadoCivilUnion()) ? request.Resolver
 1182                    TieneHijosACargo = request.TieneHijosACargo,
 1183                    CantidadHijosMenores = request.CantidadHijosMenores,
 1184                    CantidadHijosDiscapacidad = request.CantidadHijosDiscapacidad,
 1185                    PrimaSolidariaFamiliarTipo = request.PrimaSolidariaFamiliarTipo,
 1186                    TieneJustificacionRetroactivo = request.TieneJustificacionRetroactivo ?? false,
 1187                    JustificacionRetroactivo = request.JustificacionRetroactivo,
 1188                    PendienteDocumentacion = request.PendienteDocumentacion ?? (entidad.Estado == EstadoPendienteDoc)
 1189                }
 1190            };
 191
 1192            var validacion = ValidarReglasPorTipo(tipo, requestConsolidado, esActualizacion: true);
 1193            if (!string.IsNullOrEmpty(validacion))
 194            {
 0195                return (false, null, validacion);
 196            }
 197
 1198            var claveEvento = ConstruirClaveEvento(tipo, fechaEvento, vigenteDesde);
 1199            if (EsBeneficioUnicaVez(tipo)
 1200                && await _beneficioSocialRepository.ExisteDuplicadoEventoAsync(entidad.PersonaId, entidad.TipoBeneficioI
 201            {
 0202                return (false, null, "Existe un beneficio duplicado para un evento de unica vez");
 203            }
 204
 1205            var retroactividadPendiente = CalcularRetroactividadPendiente(vigenteDesde, primerDiaPeriodo);
 1206            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 207            {
 0208                return (false, null, "El tipo de beneficio no permite retroactividad para la vigencia indicada");
 209            }
 210
 1211            entidad.Cantidad = requestConsolidado.Cantidad ?? entidad.Cantidad;
 1212            entidad.VigenteDesde = vigenteDesde;
 1213            entidad.VigenteHasta = requestConsolidado.VigenteHasta?.Date;
 1214            entidad.DocumentoRespaldo = requestConsolidado.DocumentoRespaldo?.Trim();
 1215            entidad.Observaciones = ConstruirObservaciones(requestConsolidado.Observaciones, requestConsolidado.DatosTip
 1216            entidad.Estado = requestConsolidado.DatosTipo.PendienteDocumentacion ? EstadoPendienteDoc : EstadoActivo;
 1217            entidad.FechaEvento = fechaEvento;
 1218            entidad.ClaveEvento = claveEvento;
 1219            entidad.RetroactividadPendiente = retroactividadPendiente;
 220
 1221            var actualizado = await _beneficioSocialRepository.ActualizarAsync(entidad);
 222
 1223            await _auditoriaService.LogAuditoriaAsync(
 1224                usuarioId,
 1225                AccionEnum.ActualizarBeneficio,
 1226                ContextoEnum.Beneficios,
 1227                host: host,
 1228                entidad: nameof(BeneficioSocial),
 1229                entidadId: actualizado.Id,
 1230                detalle: new { operacion = "actualizar", id = actualizado.Id, personaId = actualizado.PersonaId, tipoBen
 231
 1232            return (true, MapearDto(actualizado, tipo), null);
 233        }
 0234        catch (Exception ex)
 235        {
 0236            return (false, null, $"Error al actualizar beneficio social: {ex.Message}");
 237        }
 2238    }
 239
 240    public async Task<(bool Exito, bool BajaLogica, string? Error)> EliminarAsync(long id, long usuarioId, string host)
 241    {
 2242        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 243        try
 244        {
 2245            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 2246            if (entidad == null)
 247            {
 0248                return (false, false, "Beneficio social no encontrado");
 249            }
 250
 2251            if (entidad.Estado == EstadoInactivo)
 252            {
 1253                return (true, true, null);
 254            }
 255
 1256            entidad.Estado = EstadoInactivo;
 1257            entidad.VigenteHasta ??= DateTime.Now.Date;
 1258            await _beneficioSocialRepository.ActualizarAsync(entidad);
 259
 1260            await _auditoriaService.LogAuditoriaAsync(
 1261                usuarioId,
 1262                AccionEnum.EliminarBeneficio,
 1263                ContextoEnum.Beneficios,
 1264                host: host,
 1265                entidad: nameof(BeneficioSocial),
 1266                entidadId: entidad.Id,
 1267                detalle: new { operacion = "eliminar", id = entidad.Id, personaId = entidad.PersonaId, tipoBeneficioId =
 268
 1269            return (true, true, null);
 270        }
 0271        catch (Exception ex)
 272        {
 0273            return (false, false, $"Error al eliminar beneficio social: {ex.Message}");
 274        }
 2275    }
 276
 277    private static string? ValidarReglasPorTipo(TipoBeneficio tipo, CrearBeneficioSocialRequest request, bool esActualiz
 278    {
 3279        var datosTipo = request.DatosTipo ?? new DatosTipoBeneficioRequest();
 280
 3281        if (request.Cantidad.HasValue && request.Cantidad.Value < 0)
 282        {
 0283            return "La cantidad no puede ser negativa";
 284        }
 285
 3286        if (request.VigenteHasta.HasValue && request.VigenteHasta.Value.Date < request.VigenteDesde.Date)
 287        {
 0288            return "La vigencia hasta no puede ser anterior a la vigencia desde";
 289        }
 290
 3291        var categoria = DeterminarCategoria(tipo);
 292        switch (categoria)
 293        {
 294            case CategoriaTipoBeneficio.AsignacionFamiliar:
 295                // Los hijos se registran individualmente en beneficio_social_beneficiarios.
 296                // Los documentos se suben vía /beneficiarios/{id}/documentos.
 297                // No se requieren campos en DatosTipo ni DocumentoRespaldo al crear.
 298                break;
 299
 300            case CategoriaTipoBeneficio.HogarConstituido:
 0301                if (string.IsNullOrWhiteSpace(datosTipo.EstadoCivilUnion))
 302                {
 0303                    return "Debe indicar estado civil o union concubinaria para hogar constituido";
 304                }
 305                break;
 306
 307            case CategoriaTipoBeneficio.PrimaSolidariaFamiliar:
 308                // El tipo PSF se determina por Cantidad (1=soltero, 2=casado, 3=con hijos).
 309                // Se valida que la cantidad esté en rango; el procesador emite incidencia si no.
 1310                var cantidadPsf = request.Cantidad.GetValueOrDefault(1);
 1311                if (cantidadPsf < 1 || cantidadPsf > 3)
 312                {
 0313                    return "La cantidad para Prima Solidaria Familiar debe ser 1, 2 o 3 (soltero, casado, con hijos)";
 314                }
 315                break;
 316
 317            case CategoriaTipoBeneficio.PrimaNacimiento:
 1318                if (!datosTipo.FechaEvento.HasValue)
 319                {
 0320                    return "Debe indicar la fecha de nacimiento para prima por nacimiento";
 321                }
 322
 1323                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 324                {
 0325                    return "La solicitud de prima por nacimiento debe presentarse dentro de los 60 días del nacimiento. 
 326                }
 327                break;
 328
 329            case CategoriaTipoBeneficio.PrimaMatrimonio:
 1330                if (!datosTipo.FechaEvento.HasValue)
 331                {
 0332                    return "Debe indicar la fecha de matrimonio o union";
 333                }
 334
 1335                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 336                {
 0337                    return "La solicitud de prima por matrimonio debe presentarse dentro de los 60 días del matrimonio. 
 338                }
 339                break;
 340
 341            case CategoriaTipoBeneficio.General:
 0342                if (!esActualizacion && request.Cantidad.GetValueOrDefault(1) <= 0)
 343                {
 0344                    return "La cantidad debe ser mayor a cero";
 345                }
 346                break;
 347        }
 348
 3349        return null;
 350    }
 351
 352    private static CategoriaBeneficioRequest ConvertirCategoriaRequest(CategoriaTipoBeneficio categoria)
 353    {
 4354        return categoria switch
 4355        {
 0356            CategoriaTipoBeneficio.AsignacionFamiliar => CategoriaBeneficioRequest.AsignacionFamiliar,
 0357            CategoriaTipoBeneficio.HogarConstituido => CategoriaBeneficioRequest.HogarConstituido,
 1358            CategoriaTipoBeneficio.PrimaSolidariaFamiliar => CategoriaBeneficioRequest.PrimaSolidariaFamiliar,
 2359            CategoriaTipoBeneficio.PrimaNacimiento => CategoriaBeneficioRequest.PrimaNacimiento,
 1360            CategoriaTipoBeneficio.PrimaMatrimonio => CategoriaBeneficioRequest.PrimaMatrimonio,
 0361            _ => CategoriaBeneficioRequest.General
 4362        };
 363    }
 364
 365    private static string ConstruirObservaciones(string? observaciones, string? justificacionRetroactivo)
 366    {
 2367        var observacionNormalizada = observaciones?.Trim();
 2368        var justificacionNormalizada = justificacionRetroactivo?.Trim();
 369
 2370        if (string.IsNullOrWhiteSpace(justificacionNormalizada))
 371        {
 2372            return observacionNormalizada ?? string.Empty;
 373        }
 374
 0375        if (string.IsNullOrWhiteSpace(observacionNormalizada))
 376        {
 0377            return $"Justificacion retroactivo: {justificacionNormalizada}";
 378        }
 379
 0380        return $"{observacionNormalizada} | Justificacion retroactivo: {justificacionNormalizada}";
 381    }
 382
 383    private static bool RequiereJustificacionFueraPlazo(DateTime fechaEvento, DateTime vigenteDesde)
 384    {
 0385        return (vigenteDesde.Date - fechaEvento.Date).TotalDays > 90;
 386    }
 387
 388    private static bool CalcularRetroactividadPendiente(DateTime vigenteDesde, DateTime primerDiaPeriodoAbierto)
 389    {
 390        // Un beneficio es retroactivo solo si su fecha es anterior al período de liquidación ABIERTO.
 391        // Registrar un beneficio dentro del período abierto (aunque sea en el pasado del calendario)
 392        // NO es retroactividad â€” es la carga normal del período.
 2393        return vigenteDesde.Date < primerDiaPeriodoAbierto;
 394    }
 395
 396    private static bool EsBeneficioUnicaVez(TipoBeneficio tipo)
 397    {
 3398        var categoria = DeterminarCategoria(tipo);
 3399        return categoria == CategoriaTipoBeneficio.HogarConstituido
 3400               || categoria == CategoriaTipoBeneficio.PrimaNacimiento
 3401               || categoria == CategoriaTipoBeneficio.PrimaMatrimonio;
 402    }
 403
 404    private static string ConstruirClaveEvento(TipoBeneficio tipo, DateTime? fechaEvento, DateTime vigenteDesde)
 405    {
 3406        var categoria = DeterminarCategoria(tipo);
 407
 3408        return categoria switch
 3409        {
 1410            CategoriaTipoBeneficio.PrimaNacimiento => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 1411            CategoriaTipoBeneficio.PrimaMatrimonio => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 0412            CategoriaTipoBeneficio.HogarConstituido => "UNICA",
 1413            _ => vigenteDesde.Date.ToString("yyyy-MM")
 3414        };
 415    }
 416
 417    private static CategoriaTipoBeneficio DeterminarCategoria(TipoBeneficio tipo)
 418    {
 13419        var codigo = (tipo.Codigo ?? string.Empty).Trim().ToUpperInvariant();
 13420        var nombre = (tipo.Nombre ?? string.Empty).Trim().ToUpperInvariant();
 421
 13422        if (codigo == "ASIGNACION_FAMILIAR"
 13423            || codigo.StartsWith("300") || codigo.StartsWith("301") || codigo.StartsWith("302")
 13424            || codigo.StartsWith("303") || codigo.StartsWith("304") || codigo.StartsWith("305")
 13425            || codigo.StartsWith("306") || codigo.StartsWith("307") || codigo.StartsWith("308")
 13426            || codigo.StartsWith("309") || codigo.StartsWith("310")
 13427            || nombre.Contains("ASIGNACION FAMILIAR") || nombre.Contains("ASIGNACIÓN FAMILIAR"))
 428        {
 0429            return CategoriaTipoBeneficio.AsignacionFamiliar;
 430        }
 431
 13432        if (codigo == "HOGAR_CONSTITUIDO" || codigo == "320" || nombre.Contains("HOGAR"))
 433        {
 0434            return CategoriaTipoBeneficio.HogarConstituido;
 435        }
 436
 13437        if (codigo == "PRIMA_SOLIDARIA_FAMILIAR" || codigo == "330"
 13438            || nombre.Contains("SOLIDARIA") || nombre.Contains("PSF"))
 439        {
 4440            return CategoriaTipoBeneficio.PrimaSolidariaFamiliar;
 441        }
 442
 9443        if (codigo == "PRIMA_NACIMIENTO" || codigo == "340"
 9444            || (nombre.Contains("NACIMIENTO") && !nombre.Contains("MATRIMONIO")))
 445        {
 5446            return CategoriaTipoBeneficio.PrimaNacimiento;
 447        }
 448
 4449        if (codigo == "PRIMA_MATRIMONIO" || codigo == "350"
 4450            || (nombre.Contains("MATRIMONIO") && !nombre.Contains("NACIMIENTO"))
 4451            || nombre.Contains("UNION"))
 452        {
 4453            return CategoriaTipoBeneficio.PrimaMatrimonio;
 454        }
 455
 456        // PRIMA_NACIMIENTO_MATRIMONIO: el subtipo determina si es nacimiento o matrimonio.
 457        // Se valida como General para que el frontend pueda declarar PrimaNacimiento o PrimaMatrimonio
 458        // según el subtipo enviado. El procesador lo resuelve internamente.
 0459        if (codigo == "PRIMA_NACIMIENTO_MATRIMONIO"
 0460            || (nombre.Contains("NACIMIENTO") && nombre.Contains("MATRIMONIO")))
 461        {
 0462            return CategoriaTipoBeneficio.General;
 463        }
 464
 0465        return CategoriaTipoBeneficio.General;
 466    }
 467
 468    private static BeneficioSocialDto MapearDto(BeneficioSocial entidad, TipoBeneficio? tipo = null)
 469    {
 4470        var tipoBeneficio = tipo ?? entidad.TipoBeneficio;
 471
 4472        return new BeneficioSocialDto
 4473        {
 4474            Id = entidad.Id,
 4475            PersonaId = entidad.PersonaId,
 4476            PersonaCedula = entidad.Persona?.Cedula ?? string.Empty,
 4477            PersonaNombre = entidad.Persona?.NombreCompleto ?? string.Empty,
 4478            TipoBeneficioId = entidad.TipoBeneficioId,
 4479            TipoBeneficioCodigo = tipoBeneficio?.Codigo ?? string.Empty,
 4480            TipoBeneficioNombre = tipoBeneficio?.Nombre ?? string.Empty,
 4481            Cantidad = entidad.Cantidad,
 4482            VigenteDesde = entidad.VigenteDesde,
 4483            VigenteHasta = entidad.VigenteHasta,
 4484            Estado = entidad.Estado,
 4485            DocumentoRespaldo = entidad.DocumentoRespaldo,
 4486            Observaciones = entidad.Observaciones,
 4487            FechaEvento = entidad.FechaEvento,
 4488            ClaveEvento = entidad.ClaveEvento,
 4489            RetroactividadPendiente = entidad.RetroactividadPendiente,
 4490            Subtipo = entidad.Subtipo
 4491        };
 492    }
 493
 494    private enum CategoriaTipoBeneficio
 495    {
 496        General = 0,
 497        AsignacionFamiliar = 1,
 498        HogarConstituido = 2,
 499        PrimaSolidariaFamiliar = 3,
 500        PrimaNacimiento = 4,
 501        PrimaMatrimonio = 5
 502    }
 503
 504    public async Task<string> SubirDocumentoAsync(long beneficioId, Stream contenido, string nombreOriginal, string cont
 505    {
 0506        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0507            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 508
 0509        var ext = Path.GetExtension(nombreOriginal);
 0510        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 0511            .Replace(" ", "_").Replace("..", "") + ext;
 0512        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 513
 0514        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 0515        entidad.DocumentoRespaldo = rutaRelativa;
 0516        await _beneficioSocialRepository.ActualizarAsync(entidad);
 0517        return rutaRelativa;
 0518    }
 519
 520    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoAsync(long benefi
 521    {
 0522        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0523            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 524
 0525        if (string.IsNullOrEmpty(entidad.DocumentoRespaldo))
 0526            throw new KeyNotFoundException("El beneficio no tiene documento adjunto.");
 527
 0528        var stream = await _storage.ObtenerAsync(entidad.DocumentoRespaldo);
 0529        var nombre = Path.GetFileName(entidad.DocumentoRespaldo);
 0530        var ext = Path.GetExtension(nombre).ToLowerInvariant();
 0531        var contentType = ext switch
 0532        {
 0533            ".pdf"  => "application/pdf",
 0534            ".jpg" or ".jpeg" => "image/jpeg",
 0535            ".png"  => "image/png",
 0536            ".tif" or ".tiff" => "image/tiff",
 0537            ".doc"  => "application/msword",
 0538            ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 0539            _       => "application/octet-stream"
 0540        };
 0541        return (stream, nombre, contentType);
 0542    }
 543
 544    public async Task<string> AgregarDocumentoAsync(long beneficioId, Stream contenido, string nombreOriginal, string co
 545    {
 0546        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0547            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 548
 0549        var ext              = Path.GetExtension(nombreOriginal);
 0550        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 0551                                   .Replace(" ", "_").Replace("..", "") + ext;
 0552        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 0553        var tamanio      = contenido.CanSeek ? contenido.Length : 0;
 554
 0555        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 556
 0557        await _docRepo.CreateAsync(new Documento
 0558        {
 0559            Tipo           = "beneficio_social",
 0560            EntidadId      = beneficioId,
 0561            RutaRelativa   = rutaRelativa,
 0562            NombreOriginal = nombreOriginal,
 0563            ContentType    = contentType,
 0564            TamanioBytes   = tamanio,
 0565            UsuarioCargaId = usuarioId
 0566        });
 567
 0568        return rutaRelativa;
 0569    }
 570
 571    public async Task<IEnumerable<DocumentoDto>> ObtenerDocumentosAsync(long beneficioId)
 572    {
 0573        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0574            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 575
 0576        var docs = await _docRepo.GetByTipoEntidadAsync("beneficio_social", beneficioId);
 0577        return docs.Select(d => new DocumentoDto
 0578        {
 0579            Id             = d.Id,
 0580            NombreOriginal = d.NombreOriginal,
 0581            ContentType    = d.ContentType,
 0582            TamanioBytes   = d.TamanioBytes,
 0583            FechaCarga     = d.FechaCarga
 0584        });
 0585    }
 586
 587    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoPorIdAsync(long b
 588    {
 0589        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0590            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 591
 0592        var doc = await _docRepo.GetByIdAsync(documentoId)
 0593            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 594
 0595        var stream = await _storage.ObtenerAsync(doc.RutaRelativa);
 0596        return (stream, doc.NombreOriginal, doc.ContentType);
 0597    }
 598
 599    public async Task EliminarDocumentoAsync(long beneficioId, long documentoId)
 600    {
 0601        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0602            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 603
 0604        var doc = await _docRepo.GetByIdAsync(documentoId)
 0605            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 606
 0607        try { await _storage.EliminarAsync(doc.RutaRelativa); }
 0608        catch { /* archivo físico ya eliminado, igual borramos el registro */ }
 609
 0610        await _docRepo.DeleteAsync(documentoId);
 0611    }
 612}