< 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
94%
Covered lines: 323
Uncovered lines: 20
Coverable lines: 343
Total lines: 612
Line coverage: 94.1%
Branch coverage
77%
Covered branches: 241
Total branches: 310
Branch coverage: 77.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ObtenerPorIdAsync()100%22100%
ObtenerPaginadoAsync()83.33%66100%
CrearAsync()83.33%363696.22%
ActualizarAsync()77.08%484895.52%
EliminarAsync()100%8890.47%
ValidarReglasPorTipo(...)97.14%3535100%
ConvertirCategoriaRequest(...)100%66100%
ConstruirObservaciones(...)60%181057.14%
RequiereJustificacionFueraPlazo(...)100%210%
CalcularRetroactividadPendiente(...)100%11100%
EsBeneficioUnicaVez(...)100%44100%
ConstruirClaveEvento(...)61.53%131387.5%
DeterminarCategoria(...)91.17%686896%
MapearDto(...)88.88%1818100%
SubirDocumentoAsync()100%22100%
DescargarDocumentoAsync()20%904068.42%
AgregarDocumentoAsync()75%44100%
ObtenerDocumentosAsync()100%22100%
DescargarDocumentoPorIdAsync()100%44100%
EliminarDocumentoAsync()100%4487.5%

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
 4519    public BeneficioSocialService(
 4520        IBeneficioSocialRepository beneficioSocialRepository,
 4521        IAuditoriaService auditoriaService,
 4522        IPeriodoGuard periodoGuard,
 4523        IStorageService storage,
 4524        IDocumentoRepository docRepo)
 25    {
 4526        _beneficioSocialRepository = beneficioSocialRepository;
 4527        _auditoriaService = auditoriaService;
 4528        _periodoGuard = periodoGuard;
 4529        _storage = storage;
 4530        _docRepo = docRepo;
 4531    }
 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    {
 341        if (pagina < 1) pagina = 1;
 242        if (tamano < 1) tamano = 10;
 443        if (tamano > 100) tamano = 100;
 44
 245        var (items, totalCount) = await _beneficioSocialRepository.ObtenerPaginadoAsync(pagina, tamano, personaId, estad
 46
 247        return new PagedResult<BeneficioSocialDto>
 248        {
 149            Items = items.Select(item => MapearDto(item)).ToList(),
 250            Page = pagina,
 251            PageSize = tamano,
 252            TotalCount = totalCount
 253        };
 254    }
 55
 56    public async Task<(bool Exito, BeneficioSocialDto? Resultado, string? Error)> CrearAsync(CrearBeneficioSocialRequest
 57    {
 1758        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 1759        var primerDiaPeriodo = await _periodoGuard.ObtenerPrimerDiaPeriodoAbiertoAsync();
 60        try
 61        {
 1762            var tipo = await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(request.TipoBeneficioId);
 1763            if (tipo == null || !tipo.Activo)
 64            {
 265                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 66            }
 67
 1568            var categoriaEsperada = ConvertirCategoriaRequest(DeterminarCategoria(tipo));
 1569            if (request.CategoriaBeneficio != categoriaEsperada)
 70            {
 171                return (false, null, "La categoria declarada no coincide con el tipo de beneficio seleccionado");
 72            }
 73
 1474            var personaActiva = await _beneficioSocialRepository.ExistePersonaActivaAsync(request.PersonaId, request.Vig
 1475            if (!personaActiva)
 76            {
 177                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 78            }
 79
 1380            var validacion = ValidarReglasPorTipo(tipo, request, esActualizacion: false);
 1381            if (!string.IsNullOrEmpty(validacion))
 82            {
 983                return (false, null, validacion);
 84            }
 85
 86            // Para beneficios mensuales: no permitir más de uno activo del mismo tipo por persona
 487            if (tipo.EsMensual && await _beneficioSocialRepository.ExisteBeneficioActivoMismoTipoAsync(request.PersonaId
 88            {
 189                return (false, null, $"La persona ya tiene un beneficio activo del tipo '{tipo.Nombre}'. Debe darlo de b
 90            }
 91
 392            var claveEvento = ConstruirClaveEvento(tipo, request.DatosTipo.FechaEvento, request.VigenteDesde);
 393            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
 298            var retroactividadPendiente = CalcularRetroactividadPendiente(request.VigenteDesde, primerDiaPeriodo);
 299            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 100            {
 1101                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        }
 17140    }
 141
 142    public async Task<(bool Exito, BeneficioSocialDto? Resultado, string? Error)> ActualizarAsync(long id, ActualizarBen
 143    {
 6144        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 6145        var primerDiaPeriodo = await _periodoGuard.ObtenerPrimerDiaPeriodoAbiertoAsync();
 146        try
 147        {
 6148            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 6149            if (entidad == null)
 150            {
 1151                return (false, null, "Beneficio social no encontrado");
 152            }
 153
 5154            var tipo = entidad.TipoBeneficio ?? await _beneficioSocialRepository.ObtenerTipoBeneficioAsync(entidad.TipoB
 5155            if (tipo == null || !tipo.Activo)
 156            {
 1157                return (false, null, "Tipo de beneficio no encontrado o inactivo");
 158            }
 159
 4160            var vigenteDesde = request.VigenteDesde?.Date ?? entidad.VigenteDesde.Date;
 4161            var fechaEvento = (request.FechaEvento ?? request.DatosTipo?.FechaEvento)?.Date ?? entidad.FechaEvento;
 162
 4163            if (!await _beneficioSocialRepository.ExistePersonaActivaAsync(entidad.PersonaId, vigenteDesde))
 164            {
 1165                return (false, null, "La persona no tiene relacion laboral activa para la vigencia indicada");
 166            }
 167
 3168            var requestConsolidado = new CrearBeneficioSocialRequest
 3169            {
 3170                PersonaId = entidad.PersonaId,
 3171                TipoBeneficioId = entidad.TipoBeneficioId,
 3172                Cantidad = request.Cantidad ?? entidad.Cantidad,
 3173                VigenteDesde = vigenteDesde,
 3174                VigenteHasta = request.VigenteHasta ?? entidad.VigenteHasta,
 3175                DocumentoRespaldo = request.DocumentoRespaldo ?? entidad.DocumentoRespaldo,
 3176                Observaciones = request.Observaciones ?? entidad.Observaciones,
 3177                CategoriaBeneficio = ConvertirCategoriaRequest(DeterminarCategoria(tipo)),
 3178                DatosTipo = new DatosTipoBeneficioRequest
 3179                {
 3180                    FechaEvento = fechaEvento,
 3181                    EstadoCivilUnion = !string.IsNullOrWhiteSpace(request.ResolverEstadoCivilUnion()) ? request.Resolver
 3182                    TieneHijosACargo = request.TieneHijosACargo,
 3183                    CantidadHijosMenores = request.CantidadHijosMenores,
 3184                    CantidadHijosDiscapacidad = request.CantidadHijosDiscapacidad,
 3185                    PrimaSolidariaFamiliarTipo = request.PrimaSolidariaFamiliarTipo,
 3186                    TieneJustificacionRetroactivo = request.TieneJustificacionRetroactivo ?? false,
 3187                    JustificacionRetroactivo = request.JustificacionRetroactivo,
 3188                    PendienteDocumentacion = request.PendienteDocumentacion ?? (entidad.Estado == EstadoPendienteDoc)
 3189                }
 3190            };
 191
 3192            var validacion = ValidarReglasPorTipo(tipo, requestConsolidado, esActualizacion: true);
 3193            if (!string.IsNullOrEmpty(validacion))
 194            {
 0195                return (false, null, validacion);
 196            }
 197
 3198            var claveEvento = ConstruirClaveEvento(tipo, fechaEvento, vigenteDesde);
 3199            if (EsBeneficioUnicaVez(tipo)
 3200                && await _beneficioSocialRepository.ExisteDuplicadoEventoAsync(entidad.PersonaId, entidad.TipoBeneficioI
 201            {
 1202                return (false, null, "Existe un beneficio duplicado para un evento de unica vez");
 203            }
 204
 2205            var retroactividadPendiente = CalcularRetroactividadPendiente(vigenteDesde, primerDiaPeriodo);
 2206            if (retroactividadPendiente && !tipo.PermiteRetroactividad)
 207            {
 1208                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        }
 6238    }
 239
 240    public async Task<(bool Exito, bool BajaLogica, string? Error)> EliminarAsync(long id, long usuarioId, string host)
 241    {
 3242        await _periodoGuard.VerificarPeriodoAbiertoAsync();
 243        try
 244        {
 3245            var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(id);
 3246            if (entidad == null)
 247            {
 1248                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        }
 3275    }
 276
 277    private static string? ValidarReglasPorTipo(TipoBeneficio tipo, CrearBeneficioSocialRequest request, bool esActualiz
 278    {
 16279        var datosTipo = request.DatosTipo ?? new DatosTipoBeneficioRequest();
 280
 16281        if (request.Cantidad.HasValue && request.Cantidad.Value < 0)
 282        {
 1283            return "La cantidad no puede ser negativa";
 284        }
 285
 15286        if (request.VigenteHasta.HasValue && request.VigenteHasta.Value.Date < request.VigenteDesde.Date)
 287        {
 1288            return "La vigencia hasta no puede ser anterior a la vigencia desde";
 289        }
 290
 14291        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:
 1301                if (string.IsNullOrWhiteSpace(datosTipo.EstadoCivilUnion))
 302                {
 1303                    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.
 2310                var cantidadPsf = request.Cantidad.GetValueOrDefault(1);
 2311                if (cantidadPsf < 1 || cantidadPsf > 3)
 312                {
 1313                    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:
 3318                if (!datosTipo.FechaEvento.HasValue)
 319                {
 1320                    return "Debe indicar la fecha de nacimiento para prima por nacimiento";
 321                }
 322
 2323                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 324                {
 1325                    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:
 4330                if (!datosTipo.FechaEvento.HasValue)
 331                {
 1332                    return "Debe indicar la fecha de matrimonio o union";
 333                }
 334
 3335                if ((request.VigenteDesde.Date - datosTipo.FechaEvento.Value.Date).TotalDays > 60)
 336                {
 1337                    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:
 1342                if (!esActualizacion && request.Cantidad.GetValueOrDefault(1) <= 0)
 343                {
 1344                    return "La cantidad debe ser mayor a cero";
 345                }
 346                break;
 347        }
 348
 7349        return null;
 350    }
 351
 352    private static CategoriaBeneficioRequest ConvertirCategoriaRequest(CategoriaTipoBeneficio categoria)
 353    {
 18354        return categoria switch
 18355        {
 5356            CategoriaTipoBeneficio.AsignacionFamiliar => CategoriaBeneficioRequest.AsignacionFamiliar,
 2357            CategoriaTipoBeneficio.HogarConstituido => CategoriaBeneficioRequest.HogarConstituido,
 2358            CategoriaTipoBeneficio.PrimaSolidariaFamiliar => CategoriaBeneficioRequest.PrimaSolidariaFamiliar,
 4359            CategoriaTipoBeneficio.PrimaNacimiento => CategoriaBeneficioRequest.PrimaNacimiento,
 4360            CategoriaTipoBeneficio.PrimaMatrimonio => CategoriaBeneficioRequest.PrimaMatrimonio,
 1361            _ => CategoriaBeneficioRequest.General
 18362        };
 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.
 4393        return vigenteDesde.Date < primerDiaPeriodoAbierto;
 394    }
 395
 396    private static bool EsBeneficioUnicaVez(TipoBeneficio tipo)
 397    {
 6398        var categoria = DeterminarCategoria(tipo);
 6399        return categoria == CategoriaTipoBeneficio.HogarConstituido
 6400               || categoria == CategoriaTipoBeneficio.PrimaNacimiento
 6401               || categoria == CategoriaTipoBeneficio.PrimaMatrimonio;
 402    }
 403
 404    private static string ConstruirClaveEvento(TipoBeneficio tipo, DateTime? fechaEvento, DateTime vigenteDesde)
 405    {
 6406        var categoria = DeterminarCategoria(tipo);
 407
 6408        return categoria switch
 6409        {
 1410            CategoriaTipoBeneficio.PrimaNacimiento => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 2411            CategoriaTipoBeneficio.PrimaMatrimonio => fechaEvento?.Date.ToString("yyyy-MM-dd") ?? vigenteDesde.Date.ToSt
 0412            CategoriaTipoBeneficio.HogarConstituido => "UNICA",
 3413            _ => vigenteDesde.Date.ToString("yyyy-MM")
 6414        };
 415    }
 416
 417    private static CategoriaTipoBeneficio DeterminarCategoria(TipoBeneficio tipo)
 418    {
 44419        var codigo = (tipo.Codigo ?? string.Empty).Trim().ToUpperInvariant();
 44420        var nombre = (tipo.Nombre ?? string.Empty).Trim().ToUpperInvariant();
 421
 44422        if (codigo == "ASIGNACION_FAMILIAR"
 44423            || codigo.StartsWith("300") || codigo.StartsWith("301") || codigo.StartsWith("302")
 44424            || codigo.StartsWith("303") || codigo.StartsWith("304") || codigo.StartsWith("305")
 44425            || codigo.StartsWith("306") || codigo.StartsWith("307") || codigo.StartsWith("308")
 44426            || codigo.StartsWith("309") || codigo.StartsWith("310")
 44427            || nombre.Contains("ASIGNACION FAMILIAR") || nombre.Contains("ASIGNACIÓN FAMILIAR"))
 428        {
 12429            return CategoriaTipoBeneficio.AsignacionFamiliar;
 430        }
 431
 32432        if (codigo == "HOGAR_CONSTITUIDO" || codigo == "320" || nombre.Contains("HOGAR"))
 433        {
 3434            return CategoriaTipoBeneficio.HogarConstituido;
 435        }
 436
 29437        if (codigo == "PRIMA_SOLIDARIA_FAMILIAR" || codigo == "330"
 29438            || nombre.Contains("SOLIDARIA") || nombre.Contains("PSF"))
 439        {
 6440            return CategoriaTipoBeneficio.PrimaSolidariaFamiliar;
 441        }
 442
 23443        if (codigo == "PRIMA_NACIMIENTO" || codigo == "340"
 23444            || (nombre.Contains("NACIMIENTO") && !nombre.Contains("MATRIMONIO")))
 445        {
 9446            return CategoriaTipoBeneficio.PrimaNacimiento;
 447        }
 448
 14449        if (codigo == "PRIMA_MATRIMONIO" || codigo == "350"
 14450            || (nombre.Contains("MATRIMONIO") && !nombre.Contains("NACIMIENTO"))
 14451            || nombre.Contains("UNION"))
 452        {
 12453            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.
 2459        if (codigo == "PRIMA_NACIMIENTO_MATRIMONIO"
 2460            || (nombre.Contains("NACIMIENTO") && nombre.Contains("MATRIMONIO")))
 461        {
 0462            return CategoriaTipoBeneficio.General;
 463        }
 464
 2465        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    {
 2506        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 2507            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 508
 1509        var ext = Path.GetExtension(nombreOriginal);
 1510        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 1511            .Replace(" ", "_").Replace("..", "") + ext;
 1512        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 513
 1514        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 1515        entidad.DocumentoRespaldo = rutaRelativa;
 1516        await _beneficioSocialRepository.ActualizarAsync(entidad);
 1517        return rutaRelativa;
 1518    }
 519
 520    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoAsync(long benefi
 521    {
 3522        var entidad = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 3523            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 524
 2525        if (string.IsNullOrEmpty(entidad.DocumentoRespaldo))
 1526            throw new KeyNotFoundException("El beneficio no tiene documento adjunto.");
 527
 1528        var stream = await _storage.ObtenerAsync(entidad.DocumentoRespaldo);
 1529        var nombre = Path.GetFileName(entidad.DocumentoRespaldo);
 1530        var ext = Path.GetExtension(nombre).ToLowerInvariant();
 1531        var contentType = ext switch
 1532        {
 1533            ".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"
 1540        };
 1541        return (stream, nombre, contentType);
 1542    }
 543
 544    public async Task<string> AgregarDocumentoAsync(long beneficioId, Stream contenido, string nombreOriginal, string co
 545    {
 2546        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 2547            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 548
 1549        var ext              = Path.GetExtension(nombreOriginal);
 1550        var nombreSanitizado = Path.GetFileNameWithoutExtension(nombreOriginal)
 1551                                   .Replace(" ", "_").Replace("..", "") + ext;
 1552        var rutaRelativa = $"beneficios/{beneficioId}/{nombreSanitizado}";
 1553        var tamanio      = contenido.CanSeek ? contenido.Length : 0;
 554
 1555        await _storage.GuardarAsync(contenido, rutaRelativa, contentType);
 556
 1557        await _docRepo.CreateAsync(new Documento
 1558        {
 1559            Tipo           = "beneficio_social",
 1560            EntidadId      = beneficioId,
 1561            RutaRelativa   = rutaRelativa,
 1562            NombreOriginal = nombreOriginal,
 1563            ContentType    = contentType,
 1564            TamanioBytes   = tamanio,
 1565            UsuarioCargaId = usuarioId
 1566        });
 567
 1568        return rutaRelativa;
 1569    }
 570
 571    public async Task<IEnumerable<DocumentoDto>> ObtenerDocumentosAsync(long beneficioId)
 572    {
 2573        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 2574            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 575
 1576        var docs = await _docRepo.GetByTipoEntidadAsync("beneficio_social", beneficioId);
 2577        return docs.Select(d => new DocumentoDto
 2578        {
 2579            Id             = d.Id,
 2580            NombreOriginal = d.NombreOriginal,
 2581            ContentType    = d.ContentType,
 2582            TamanioBytes   = d.TamanioBytes,
 2583            FechaCarga     = d.FechaCarga
 2584        });
 1585    }
 586
 587    public async Task<(Stream Contenido, string NombreOriginal, string ContentType)> DescargarDocumentoPorIdAsync(long b
 588    {
 3589        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 3590            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 591
 2592        var doc = await _docRepo.GetByIdAsync(documentoId)
 2593            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 594
 1595        var stream = await _storage.ObtenerAsync(doc.RutaRelativa);
 1596        return (stream, doc.NombreOriginal, doc.ContentType);
 1597    }
 598
 599    public async Task EliminarDocumentoAsync(long beneficioId, long documentoId)
 600    {
 3601        _ = await _beneficioSocialRepository.ObtenerPorIdAsync(beneficioId)
 3602            ?? throw new ArgumentException($"Beneficio social {beneficioId} no encontrado.");
 603
 2604        var doc = await _docRepo.GetByIdAsync(documentoId)
 2605            ?? throw new KeyNotFoundException($"Documento {documentoId} no encontrado.");
 606
 2607        try { await _storage.EliminarAsync(doc.RutaRelativa); }
 0608        catch { /* archivo físico ya eliminado, igual borramos el registro */ }
 609
 1610        await _docRepo.DeleteAsync(documentoId);
 1611    }
 612}