< 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: 222
Uncovered lines: 119
Coverable lines: 341
Total lines: 607
Line coverage: 65.1%
Branch coverage
51%
Covered branches: 157
Total branches: 306
Branch coverage: 51.3%
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();
 59        try
 60        {
 361            var tipo = await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(request.TipoBeneficioId);
 362            if (tipo == null || !tipo.Activo)
 63            {
 064                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 65            }
 66
 367            var categoriaEsperada = ConvertirCategoriaRequest(DeterminarCategoria(tipo));
 368            if (request.CategoriaBeneficio != categoriaEsperada)
 69            {
 070                return (false, null, "La categoria declarada no coincide con el tipo de beneficio seleccionado");
 71            }
 72
 373            var personaActiva = await _beneficioSocialRepository.ExistePersonaActivaAsync(request.PersonaId, request.Vig
 374            if (!personaActiva)
 75            {
 176                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 77            }
 78
 279            var validacion = ValidarReglasPorTipo(tipo, request, esActualizacion: false);
 280            if (!string.IsNullOrEmpty(validacion))
 81            {
 082                return (false, null, validacion);
 83            }
 84
 85            // Para beneficios mensuales: no permitir más de uno activo del mismo tipo por persona
 286            if (tipo.EsMensual && await _beneficioSocialRepository.ExisteBeneficioActivoMismoTipoAsync(request.PersonaId
 87            {
 088                return (false, null, $"La persona ya tiene un beneficio activo del tipo '{tipo.Nombre}'. Debe darlo de b
 89            }
 90
 291            var claveEvento = ConstruirClaveEvento(tipo, request.DatosTipo.FechaEvento, request.VigenteDesde);
 292            if (EsBeneficioUnicaVez(tipo) && await _beneficioSocialRepository.ExisteDuplicadoEventoAsync(request.Persona
 93            {
 194                return (false, null, "Existe un beneficio duplicado para un evento de unica vez");
 95            }
 96
 197            var retroactividadPendiente = CalcularRetroactividadPendiente(request.VigenteDesde);
 198            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 99            {
 0100                return (false, null, "El tipo de beneficio no permite retroactividad para la vigencia indicada");
 101            }
 102
 1103            var estado = request.DatosTipo.PendienteDocumentacion ? EstadoPendienteDoc : EstadoActivo;
 1104            var entidad = new BeneficioSocial
 1105            {
 1106                PersonaId = request.PersonaId,
 1107                TipoBeneficioId = request.TipoBeneficioId,
 1108                Cantidad = (short)Math.Max(request.Cantidad ?? 1, (short)0),
 1109                VigenteDesde = request.VigenteDesde.Date,
 1110                VigenteHasta = request.VigenteHasta?.Date,
 1111                Estado = estado,
 1112                DocumentoRespaldo = request.DocumentoRespaldo?.Trim(),
 1113                Observaciones = ConstruirObservaciones(request.Observaciones, request.DatosTipo.JustificacionRetroactivo
 1114                CreadoPor = usuarioId,
 1115                FechaEvento = request.DatosTipo.FechaEvento?.Date,
 1116                ClaveEvento = claveEvento,
 1117                Subtipo = request.DatosTipo.Subtipo?.Trim().ToUpperInvariant(),
 1118                RetroactividadPendiente = retroactividadPendiente
 1119            };
 120
 1121            var creado = await _beneficioSocialRepository.CrearAsync(entidad);
 122
 1123            await _auditoriaService.LogAuditoriaAsync(
 1124                usuarioId,
 1125                AccionEnum.CrearBeneficio,
 1126                ContextoEnum.Beneficios,
 1127                host: host,
 1128                entidad: nameof(BeneficioSocial),
 1129                entidadId: creado.Id,
 1130                detalle: new { operacion = "crear", id = creado.Id, personaId = creado.PersonaId, tipoBeneficioId = crea
 131
 1132            var recargado = await _beneficioSocialRepository.ObtenerPorIdAsync(creado.Id);
 1133            return (true, recargado == null ? MapearDto(creado, tipo) : MapearDto(recargado), null);
 134        }
 0135        catch (Exception ex)
 136        {
 0137            return (false, null, $"Error al crear beneficio social: {ex.Message}");
 138        }
 3139    }
 140
 141    public async Task<(bool Exito, BeneficioSocialDto? Resultado, string? Error)> ActualizarAsync(long id, ActualizarBen
 142    {
 2143        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 144        try
 145        {
 2146            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 2147            if (entidad == null)
 148            {
 1149                return (false, null, "Beneficio social no encontrado");
 150            }
 151
 1152            var tipo = entidad.TipoBeneficio ?? await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(entidad.TipoB
 1153            if (tipo == null || !tipo.Activo)
 154            {
 0155                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 156            }
 157
 1158            var vigenteDesde = request.VigenteDesde?.Date ?? entidad.VigenteDesde.Date;
 1159            var fechaEvento = request.FechaEvento?.Date ?? entidad.FechaEvento;
 160
 1161            if (!await _beneficioSocialRepository.ExistePersonaActivaAsync(entidad.PersonaId, vigenteDesde))
 162            {
 0163                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 164            }
 165
 1166            var requestConsolidado = new CrearBeneficioSocialRequest
 1167            {
 1168                PersonaId = entidad.PersonaId,
 1169                TipoBeneficioId = entidad.TipoBeneficioId,
 1170                Cantidad = request.Cantidad ?? entidad.Cantidad,
 1171                VigenteDesde = vigenteDesde,
 1172                VigenteHasta = request.VigenteHasta ?? entidad.VigenteHasta,
 1173                DocumentoRespaldo = request.DocumentoRespaldo ?? entidad.DocumentoRespaldo,
 1174                Observaciones = request.Observaciones ?? entidad.Observaciones,
 1175                CategoriaBeneficio = ConvertirCategoriaRequest(DeterminarCategoria(tipo)),
 1176                DatosTipo = new DatosTipoBeneficioRequest
 1177                {
 1178                    FechaEvento = fechaEvento,
 1179                    EstadoCivilUnion = request.EstadoCivilUnion,
 1180                    TieneHijosACargo = request.TieneHijosACargo,
 1181                    CantidadHijosMenores = request.CantidadHijosMenores,
 1182                    CantidadHijosDiscapacidad = request.CantidadHijosDiscapacidad,
 1183                    PrimaSolidariaFamiliarTipo = request.PrimaSolidariaFamiliarTipo,
 1184                    TieneJustificacionRetroactivo = request.TieneJustificacionRetroactivo ?? false,
 1185                    JustificacionRetroactivo = request.JustificacionRetroactivo,
 1186                    PendienteDocumentacion = request.PendienteDocumentacion ?? (entidad.Estado == EstadoPendienteDoc)
 1187                }
 1188            };
 189
 1190            var validacion = ValidarReglasPorTipo(tipo, requestConsolidado, esActualizacion: true);
 1191            if (!string.IsNullOrEmpty(validacion))
 192            {
 0193                return (false, null, validacion);
 194            }
 195
 1196            var claveEvento = ConstruirClaveEvento(tipo, fechaEvento, vigenteDesde);
 1197            if (EsBeneficioUnicaVez(tipo)
 1198                && await _beneficioSocialRepository.ExisteDuplicadoEventoAsync(entidad.PersonaId, entidad.TipoBeneficioI
 199            {
 0200                return (false, null, "Existe un beneficio duplicado para un evento de unica vez");
 201            }
 202
 1203            var retroactividadPendiente = CalcularRetroactividadPendiente(vigenteDesde);
 1204            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 205            {
 0206                return (false, null, "El tipo de beneficio no permite retroactividad para la vigencia indicada");
 207            }
 208
 1209            entidad.Cantidad = requestConsolidado.Cantidad ?? entidad.Cantidad;
 1210            entidad.VigenteDesde = vigenteDesde;
 1211            entidad.VigenteHasta = requestConsolidado.VigenteHasta?.Date;
 1212            entidad.DocumentoRespaldo = requestConsolidado.DocumentoRespaldo?.Trim();
 1213            entidad.Observaciones = ConstruirObservaciones(requestConsolidado.Observaciones, requestConsolidado.DatosTip
 1214            entidad.Estado = requestConsolidado.DatosTipo.PendienteDocumentacion ? EstadoPendienteDoc : EstadoActivo;
 1215            entidad.FechaEvento = fechaEvento;
 1216            entidad.ClaveEvento = claveEvento;
 1217            entidad.RetroactividadPendiente = retroactividadPendiente;
 218
 1219            var actualizado = await _beneficioSocialRepository.ActualizarAsync(entidad);
 220
 1221            await _auditoriaService.LogAuditoriaAsync(
 1222                usuarioId,
 1223                AccionEnum.ActualizarBeneficio,
 1224                ContextoEnum.Beneficios,
 1225                host: host,
 1226                entidad: nameof(BeneficioSocial),
 1227                entidadId: actualizado.Id,
 1228                detalle: new { operacion = "actualizar", id = actualizado.Id, personaId = actualizado.PersonaId, tipoBen
 229
 1230            return (true, MapearDto(actualizado, tipo), null);
 231        }
 0232        catch (Exception ex)
 233        {
 0234            return (false, null, $"Error al actualizar beneficio social: {ex.Message}");
 235        }
 2236    }
 237
 238    public async Task<(bool Exito, bool BajaLogica, string? Error)> EliminarAsync(long id, long usuarioId, string host)
 239    {
 2240        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 241        try
 242        {
 2243            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 2244            if (entidad == null)
 245            {
 0246                return (false, false, "Beneficio social no encontrado");
 247            }
 248
 2249            if (entidad.Estado == EstadoInactivo)
 250            {
 1251                return (true, true, null);
 252            }
 253
 1254            entidad.Estado = EstadoInactivo;
 1255            entidad.VigenteHasta ??= DateTime.UtcNow.Date;
 1256            await _beneficioSocialRepository.ActualizarAsync(entidad);
 257
 1258            await _auditoriaService.LogAuditoriaAsync(
 1259                usuarioId,
 1260                AccionEnum.EliminarBeneficio,
 1261                ContextoEnum.Beneficios,
 1262                host: host,
 1263                entidad: nameof(BeneficioSocial),
 1264                entidadId: entidad.Id,
 1265                detalle: new { operacion = "eliminar", id = entidad.Id, personaId = entidad.PersonaId, tipoBeneficioId =
 266
 1267            return (true, true, null);
 268        }
 0269        catch (Exception ex)
 270        {
 0271            return (false, false, $"Error al eliminar beneficio social: {ex.Message}");
 272        }
 2273    }
 274
 275    private static string? ValidarReglasPorTipo(TipoBeneficio tipo, CrearBeneficioSocialRequest request, bool esActualiz
 276    {
 3277        var datosTipo = request.DatosTipo ?? new DatosTipoBeneficioRequest();
 278
 3279        if (request.Cantidad.HasValue && request.Cantidad.Value < 0)
 280        {
 0281            return "La cantidad no puede ser negativa";
 282        }
 283
 3284        if (request.VigenteHasta.HasValue && request.VigenteHasta.Value.Date < request.VigenteDesde.Date)
 285        {
 0286            return "La vigencia hasta no puede ser anterior a la vigencia desde";
 287        }
 288
 3289        var categoria = DeterminarCategoria(tipo);
 290        switch (categoria)
 291        {
 292            case CategoriaTipoBeneficio.AsignacionFamiliar:
 293                // Los hijos se registran individualmente en beneficio_social_beneficiarios.
 294                // Los documentos se suben vía /beneficiarios/{id}/documentos.
 295                // No se requieren campos en DatosTipo ni DocumentoRespaldo al crear.
 296                break;
 297
 298            case CategoriaTipoBeneficio.HogarConstituido:
 0299                if (string.IsNullOrWhiteSpace(datosTipo.EstadoCivilUnion))
 300                {
 0301                    return "Debe indicar estado civil o union concubinaria para hogar constituido";
 302                }
 303                break;
 304
 305            case CategoriaTipoBeneficio.PrimaSolidariaFamiliar:
 306                // El tipo PSF se determina por Cantidad (1=soltero, 2=casado, 3=con hijos).
 307                // Se valida que la cantidad esté en rango; el procesador emite incidencia si no.
 1308                var cantidadPsf = request.Cantidad.GetValueOrDefault(1);
 1309                if (cantidadPsf < 1 || cantidadPsf > 3)
 310                {
 0311                    return "La cantidad para Prima Solidaria Familiar debe ser 1, 2 o 3 (soltero, casado, con hijos)";
 312                }
 313                break;
 314
 315            case CategoriaTipoBeneficio.PrimaNacimiento:
 1316                if (!datosTipo.FechaEvento.HasValue)
 317                {
 0318                    return "Debe indicar la fecha de nacimiento para prima por nacimiento";
 319                }
 320
 1321                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 322                {
 0323                    return "La solicitud de prima por nacimiento debe presentarse dentro de los 60 días del nacimiento. 
 324                }
 325                break;
 326
 327            case CategoriaTipoBeneficio.PrimaMatrimonio:
 1328                if (!datosTipo.FechaEvento.HasValue)
 329                {
 0330                    return "Debe indicar la fecha de matrimonio o union";
 331                }
 332
 1333                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 334                {
 0335                    return "La solicitud de prima por matrimonio debe presentarse dentro de los 60 días del matrimonio. 
 336                }
 337                break;
 338
 339            case CategoriaTipoBeneficio.General:
 0340                if (!esActualizacion && request.Cantidad.GetValueOrDefault(1) <= 0)
 341                {
 0342                    return "La cantidad debe ser mayor a cero";
 343                }
 344                break;
 345        }
 346
 3347        return null;
 348    }
 349
 350    private static CategoriaBeneficioRequest ConvertirCategoriaRequest(CategoriaTipoBeneficio categoria)
 351    {
 4352        return categoria switch
 4353        {
 0354            CategoriaTipoBeneficio.AsignacionFamiliar => CategoriaBeneficioRequest.AsignacionFamiliar,
 0355            CategoriaTipoBeneficio.HogarConstituido => CategoriaBeneficioRequest.HogarConstituido,
 1356            CategoriaTipoBeneficio.PrimaSolidariaFamiliar => CategoriaBeneficioRequest.PrimaSolidariaFamiliar,
 2357            CategoriaTipoBeneficio.PrimaNacimiento => CategoriaBeneficioRequest.PrimaNacimiento,
 1358            CategoriaTipoBeneficio.PrimaMatrimonio => CategoriaBeneficioRequest.PrimaMatrimonio,
 0359            _ => CategoriaBeneficioRequest.General
 4360        };
 361    }
 362
 363    private static string ConstruirObservaciones(string? observaciones, string? justificacionRetroactivo)
 364    {
 2365        var observacionNormalizada = observaciones?.Trim();
 2366        var justificacionNormalizada = justificacionRetroactivo?.Trim();
 367
 2368        if (string.IsNullOrWhiteSpace(justificacionNormalizada))
 369        {
 2370            return observacionNormalizada ?? string.Empty;
 371        }
 372
 0373        if (string.IsNullOrWhiteSpace(observacionNormalizada))
 374        {
 0375            return $"Justificacion retroactivo: {justificacionNormalizada}";
 376        }
 377
 0378        return $"{observacionNormalizada} | Justificacion retroactivo: {justificacionNormalizada}";
 379    }
 380
 381    private static bool RequiereJustificacionFueraPlazo(DateTime fechaEvento, DateTime vigenteDesde)
 382    {
 0383        return (vigenteDesde.Date - fechaEvento.Date).TotalDays > 90;
 384    }
 385
 386    private static bool CalcularRetroactividadPendiente(DateTime vigenteDesde)
 387    {
 2388        var primerDiaMes = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1);
 2389        return vigenteDesde.Date < primerDiaMes;
 390    }
 391
 392    private static bool EsBeneficioUnicaVez(TipoBeneficio tipo)
 393    {
 3394        var categoria = DeterminarCategoria(tipo);
 3395        return categoria == CategoriaTipoBeneficio.HogarConstituido
 3396               || categoria == CategoriaTipoBeneficio.PrimaNacimiento
 3397               || categoria == CategoriaTipoBeneficio.PrimaMatrimonio;
 398    }
 399
 400    private static string ConstruirClaveEvento(TipoBeneficio tipo, DateTime? fechaEvento, DateTime vigenteDesde)
 401    {
 3402        var categoria = DeterminarCategoria(tipo);
 403
 3404        return categoria switch
 3405        {
 1406            CategoriaTipoBeneficio.PrimaNacimiento => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 1407            CategoriaTipoBeneficio.PrimaMatrimonio => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 0408            CategoriaTipoBeneficio.HogarConstituido => "UNICA",
 1409            _ => vigenteDesde.Date.ToString("yyyy-MM")
 3410        };
 411    }
 412
 413    private static CategoriaTipoBeneficio DeterminarCategoria(TipoBeneficio tipo)
 414    {
 13415        var codigo = (tipo.Codigo ?? string.Empty).Trim().ToUpperInvariant();
 13416        var nombre = (tipo.Nombre ?? string.Empty).Trim().ToUpperInvariant();
 417
 13418        if (codigo == "ASIGNACION_FAMILIAR"
 13419            || codigo.StartsWith("300") || codigo.StartsWith("301") || codigo.StartsWith("302")
 13420            || codigo.StartsWith("303") || codigo.StartsWith("304") || codigo.StartsWith("305")
 13421            || codigo.StartsWith("306") || codigo.StartsWith("307") || codigo.StartsWith("308")
 13422            || codigo.StartsWith("309") || codigo.StartsWith("310")
 13423            || nombre.Contains("ASIGNACION FAMILIAR") || nombre.Contains("ASIGNACIÓN FAMILIAR"))
 424        {
 0425            return CategoriaTipoBeneficio.AsignacionFamiliar;
 426        }
 427
 13428        if (codigo == "HOGAR_CONSTITUIDO" || codigo == "320" || nombre.Contains("HOGAR"))
 429        {
 0430            return CategoriaTipoBeneficio.HogarConstituido;
 431        }
 432
 13433        if (codigo == "PRIMA_SOLIDARIA_FAMILIAR" || codigo == "330"
 13434            || nombre.Contains("SOLIDARIA") || nombre.Contains("PSF"))
 435        {
 4436            return CategoriaTipoBeneficio.PrimaSolidariaFamiliar;
 437        }
 438
 9439        if (codigo == "PRIMA_NACIMIENTO" || codigo == "340"
 9440            || (nombre.Contains("NACIMIENTO") && !nombre.Contains("MATRIMONIO")))
 441        {
 5442            return CategoriaTipoBeneficio.PrimaNacimiento;
 443        }
 444
 4445        if (codigo == "PRIMA_MATRIMONIO" || codigo == "350"
 4446            || (nombre.Contains("MATRIMONIO") && !nombre.Contains("NACIMIENTO"))
 4447            || nombre.Contains("UNION"))
 448        {
 4449            return CategoriaTipoBeneficio.PrimaMatrimonio;
 450        }
 451
 452        // PRIMA_NACIMIENTO_MATRIMONIO: el subtipo determina si es nacimiento o matrimonio.
 453        // Se valida como General para que el frontend pueda declarar PrimaNacimiento o PrimaMatrimonio
 454        // según el subtipo enviado. El procesador lo resuelve internamente.
 0455        if (codigo == "PRIMA_NACIMIENTO_MATRIMONIO"
 0456            || (nombre.Contains("NACIMIENTO") && nombre.Contains("MATRIMONIO")))
 457        {
 0458            return CategoriaTipoBeneficio.General;
 459        }
 460
 0461        return CategoriaTipoBeneficio.General;
 462    }
 463
 464    private static BeneficioSocialDto MapearDto(BeneficioSocial entidad, TipoBeneficio? tipo = null)
 465    {
 4466        var tipoBeneficio = tipo ?? entidad.TipoBeneficio;
 467
 4468        return new BeneficioSocialDto
 4469        {
 4470            Id = entidad.Id,
 4471            PersonaId = entidad.PersonaId,
 4472            PersonaCedula = entidad.Persona?.Cedula ?? string.Empty,
 4473            PersonaNombre = entidad.Persona?.NombreCompleto ?? string.Empty,
 4474            TipoBeneficioId = entidad.TipoBeneficioId,
 4475            TipoBeneficioCodigo = tipoBeneficio?.Codigo ?? string.Empty,
 4476            TipoBeneficioNombre = tipoBeneficio?.Nombre ?? string.Empty,
 4477            Cantidad = entidad.Cantidad,
 4478            VigenteDesde = entidad.VigenteDesde,
 4479            VigenteHasta = entidad.VigenteHasta,
 4480            Estado = entidad.Estado,
 4481            DocumentoRespaldo = entidad.DocumentoRespaldo,
 4482            Observaciones = entidad.Observaciones,
 4483            FechaEvento = entidad.FechaEvento,
 4484            ClaveEvento = entidad.ClaveEvento,
 4485            RetroactividadPendiente = entidad.RetroactividadPendiente
 4486        };
 487    }
 488
 489    private enum CategoriaTipoBeneficio
 490    {
 491        General = 0,
 492        AsignacionFamiliar = 1,
 493        HogarConstituido = 2,
 494        PrimaSolidariaFamiliar = 3,
 495        PrimaNacimiento = 4,
 496        PrimaMatrimonio = 5
 497    }
 498
 499    public async Task<string> SubirDocumentoAsync(long beneficioId, Stream contenido, string nombreOriginal, string cont
 500    {
 0501        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0502            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 503
 0504        var ext = Path.GetExtension(nombreOriginal);
 0505        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 0506            .Replace(" ", "_").Replace("..", "") + ext;
 0507        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 508
 0509        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 0510        entidad.DocumentoRespaldo = rutaRelativa;
 0511        await _beneficioSocialRepository.ActualizarAsync(entidad);
 0512        return rutaRelativa;
 0513    }
 514
 515    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoAsync(long benefi
 516    {
 0517        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0518            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 519
 0520        if (string.IsNullOrEmpty(entidad.DocumentoRespaldo))
 0521            throw new KeyNotFoundException("El beneficio no tiene documento adjunto.");
 522
 0523        var stream = await _storage.ObtenerAsync(entidad.DocumentoRespaldo);
 0524        var nombre = Path.GetFileName(entidad.DocumentoRespaldo);
 0525        var ext = Path.GetExtension(nombre).ToLowerInvariant();
 0526        var contentType = ext switch
 0527        {
 0528            ".pdf"  => "application/pdf",
 0529            ".jpg" or ".jpeg" => "image/jpeg",
 0530            ".png"  => "image/png",
 0531            ".tif" or ".tiff" => "image/tiff",
 0532            ".doc"  => "application/msword",
 0533            ".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
 0534            _       => "application/octet-stream"
 0535        };
 0536        return (stream, nombre, contentType);
 0537    }
 538
 539    public async Task<string> AgregarDocumentoAsync(long beneficioId, Stream contenido, string nombreOriginal, string co
 540    {
 0541        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0542            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 543
 0544        var ext              = Path.GetExtension(nombreOriginal);
 0545        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 0546                                   .Replace(" ", "_").Replace("..", "") + ext;
 0547        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 0548        var tamanio      = contenido.CanSeek ? contenido.Length : 0;
 549
 0550        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 551
 0552        await _docRepo.CreateAsync(new Documento
 0553        {
 0554            Tipo           = "beneficio_social",
 0555            EntidadId      = beneficioId,
 0556            RutaRelativa   = rutaRelativa,
 0557            NombreOriginal = nombreOriginal,
 0558            ContentType    = contentType,
 0559            TamanioBytes   = tamanio,
 0560            UsuarioCargaId = usuarioId
 0561        });
 562
 0563        return rutaRelativa;
 0564    }
 565
 566    public async Task<IEnumerable<DocumentoDto>> ObtenerDocumentosAsync(long beneficioId)
 567    {
 0568        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0569            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 570
 0571        var docs = await _docRepo.GetByTipoEntidadAsync("beneficio_social", beneficioId);
 0572        return docs.Select(d => new DocumentoDto
 0573        {
 0574            Id             = d.Id,
 0575            NombreOriginal = d.NombreOriginal,
 0576            ContentType    = d.ContentType,
 0577            TamanioBytes   = d.TamanioBytes,
 0578            FechaCarga     = d.FechaCarga
 0579        });
 0580    }
 581
 582    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoPorIdAsync(long b
 583    {
 0584        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0585            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 586
 0587        var doc = await _docRepo.GetByIdAsync(documentoId)
 0588            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 589
 0590        var stream = await _storage.ObtenerAsync(doc.RutaRelativa);
 0591        return (stream, doc.NombreOriginal, doc.ContentType);
 0592    }
 593
 594    public async Task EliminarDocumentoAsync(long beneficioId, long documentoId)
 595    {
 0596        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 0597            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 598
 0599        var doc = await _docRepo.GetByIdAsync(documentoId)
 0600            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 601
 0602        try { await _storage.EliminarAsync(doc.RutaRelativa); }
 0603        catch { /* archivo físico ya eliminado, igual borramos el registro */ }
 604
 0605        await _docRepo.DeleteAsync(documentoId);
 0606    }
 607}