| | | 1 | | using FAU.DataAccess; |
| | | 2 | | using FAU.Entidades; |
| | | 3 | | using FAU.Logica.DTOs; |
| | | 4 | | using Microsoft.EntityFrameworkCore; |
| | | 5 | | |
| | | 6 | | namespace FAU.Logica.Services; |
| | | 7 | | |
| | | 8 | | public class InsumosPeriodoLoader : IInsumosPeriodoLoader |
| | | 9 | | { |
| | | 10 | | private readonly ApplicationDbContext _context; |
| | | 11 | | |
| | | 12 | | // Mapeo escalafón (código) → grupo de remuneración, hardcodeado (no en la base). |
| | | 13 | | // Confirmado con recibos reales (no solo el instructivo): el driver de Cuerpo Comando |
| | | 14 | | // vs SS.GG. Y Equiparados en 042.536/048.015 NO es el escalafón técnico de la persona |
| | | 15 | | // (AV/SG/NAV/AT/...) — personal activo en escalafón SG usa Cuerpo Comando igual que |
| | | 16 | | // cualquier otro (cédulas 53764283, 52106008, 43111769). El único caso real confirmado |
| | | 17 | | // con tarifa SS.GG. es un reservista superior (SIT05), cuyo escalafón queda tageado |
| | | 18 | | // "RVA" en este modelo (cédula 40211677). Todo lo que no sea RVA cae al valor general |
| | | 19 | | // (= Cuerpo Comando). |
| | 1 | 20 | | private static readonly Dictionary<string, string> GrupoPorCodigoEscalafon = new(StringComparer.OrdinalIgnoreCase) |
| | 1 | 21 | | { |
| | 1 | 22 | | ["RVA"] = "SSGG", |
| | 1 | 23 | | }; |
| | | 24 | | |
| | 16 | 25 | | public InsumosPeriodoLoader(ApplicationDbContext context) |
| | | 26 | | { |
| | 16 | 27 | | _context = context; |
| | 16 | 28 | | } |
| | | 29 | | |
| | | 30 | | public async Task<InsumosPeriodo> CargarAsync(long periodoId) |
| | | 31 | | { |
| | 16 | 32 | | var periodo = await _context.PeriodosLiquidacion |
| | 16 | 33 | | .AsNoTracking() |
| | 16 | 34 | | .FirstOrDefaultAsync(p => p.Id == periodoId && p.Activo); |
| | | 35 | | |
| | 16 | 36 | | if (periodo is null) |
| | | 37 | | { |
| | 0 | 38 | | throw new InvalidOperationException($"Periodo de liquidación {periodoId} no encontrado o no está activo."); |
| | | 39 | | } |
| | | 40 | | |
| | 16 | 41 | | var periodoDate = new DateTime(periodo.Anio, periodo.Mes, 1); |
| | 16 | 42 | | var ultimoDiaPeriodo = new DateTime(periodo.Anio, periodo.Mes, DateTime.DaysInMonth(periodo.Anio, periodo.Mes)); |
| | | 43 | | |
| | 16 | 44 | | var parametros = await _context.ParametrosPeriodo |
| | 16 | 45 | | .AsNoTracking() |
| | 16 | 46 | | .Where(pp => pp.Anio < periodo.Anio || (pp.Anio == periodo.Anio && pp.Mes <= periodo.Mes)) |
| | 16 | 47 | | .OrderByDescending(pp => pp.Anio) |
| | 16 | 48 | | .ThenByDescending(pp => pp.Mes) |
| | 16 | 49 | | .FirstOrDefaultAsync(); |
| | | 50 | | |
| | 16 | 51 | | if (parametros is null) |
| | | 52 | | { |
| | 0 | 53 | | throw new InvalidOperationException($"No hay parámetros de período configurados para {periodo.Anio}/{periodo |
| | | 54 | | } |
| | | 55 | | |
| | 16 | 56 | | var catalogoItems = await _context.CatalogoItems |
| | 16 | 57 | | .AsNoTracking() |
| | 16 | 58 | | .ToListAsync(); |
| | | 59 | | |
| | 16 | 60 | | var remuneraciones = await _context.RemuneracionesGrado |
| | 16 | 61 | | .AsNoTracking() |
| | 16 | 62 | | .Include(r => r.Item) |
| | 16 | 63 | | .Where(r => r.VigenteDesde <= periodoDate && (r.VigenteHasta == null || r.VigenteHasta >= periodoDate)) |
| | 16 | 64 | | .ToListAsync(); |
| | | 65 | | |
| | 16 | 66 | | var escalafones = await _context.Escalafones |
| | 16 | 67 | | .AsNoTracking() |
| | 16 | 68 | | .ToListAsync(); |
| | | 69 | | |
| | 16 | 70 | | var tarifasCategoria = await _context.TarifasPorCategoria |
| | 16 | 71 | | .AsNoTracking() |
| | 16 | 72 | | .Include(t => t.Item) |
| | 16 | 73 | | .Where(t => t.VigenteDesde <= periodoDate && (t.VigenteHasta == null || t.VigenteHasta >= periodoDate)) |
| | 16 | 74 | | .ToListAsync(); |
| | | 75 | | |
| | 16 | 76 | | var compPersonal048018 = await _context.CompPersonal048018 |
| | 16 | 77 | | .AsNoTracking() |
| | 16 | 78 | | .Where(c => c.Estado == "activo" |
| | 16 | 79 | | && c.VigenteDesde <= periodoDate && (c.VigenteHasta == null || c.VigenteHasta >= periodoDate)) |
| | 16 | 80 | | .ToListAsync(); |
| | | 81 | | |
| | 16 | 82 | | var permanencias = await _context.Set<TablaPermanencia>() |
| | 16 | 83 | | .AsNoTracking() |
| | 16 | 84 | | .Where(t => t.Activo) |
| | 16 | 85 | | .Where(t => t.VigenteDesde <= periodoDate && (t.VigenteHasta == null || t.VigenteHasta >= periodoDate)) |
| | 16 | 86 | | .ToListAsync(); |
| | | 87 | | |
| | 16 | 88 | | var franjasIrpfRaw = await _context.FranjasIrpf |
| | 16 | 89 | | .AsNoTracking() |
| | 16 | 90 | | .Where(f => f.VigenteDesde <= periodoDate && (f.VigenteHasta == null || f.VigenteHasta >= periodoDate)) |
| | 16 | 91 | | .ToListAsync(); |
| | | 92 | | |
| | | 93 | | // Deduplicar: una franja por NumeroFranja, la más reciente según VigenteDesde |
| | 16 | 94 | | var franjasIrpf = franjasIrpfRaw |
| | 32 | 95 | | .GroupBy(f => f.NumeroFranja) |
| | 64 | 96 | | .Select(g => g.OrderByDescending(f => f.VigenteDesde).First()) |
| | 32 | 97 | | .OrderBy(f => f.NumeroFranja) |
| | 16 | 98 | | .ToList(); |
| | | 99 | | |
| | | 100 | | // Tramos de Sanidad Militar vigentes (Art. 94 Ley 18.834) |
| | 16 | 101 | | var tramosSanidadRaw = await _context.TramosSanidadMilitar |
| | 16 | 102 | | .AsNoTracking() |
| | 16 | 103 | | .Where(t => t.VigenteDesde <= periodoDate && (t.VigenteHasta == null || t.VigenteHasta >= periodoDate)) |
| | 16 | 104 | | .ToListAsync(); |
| | | 105 | | |
| | | 106 | | // Deduplicar por código de retención, el más reciente según VigenteDesde; ordenar por Desde |
| | 16 | 107 | | var tramosSanidad = tramosSanidadRaw |
| | 0 | 108 | | .GroupBy(t => t.CodigoRetencion) |
| | 0 | 109 | | .Select(g => g.OrderByDescending(t => t.VigenteDesde).First()) |
| | 0 | 110 | | .OrderBy(t => t.Desde) |
| | 16 | 111 | | .ToList(); |
| | | 112 | | |
| | 16 | 113 | | var beneficios = await _context.BeneficiosSociales |
| | 16 | 114 | | .AsNoTracking() |
| | 16 | 115 | | .Where(b => b.Estado == "activo" |
| | 16 | 116 | | && b.VigenteDesde <= ultimoDiaPeriodo // inició antes del fin del período |
| | 16 | 117 | | && (b.VigenteHasta == null || b.VigenteHasta >= periodoDate)) // no terminó antes del inicio |
| | 16 | 118 | | .ToListAsync(); |
| | | 119 | | |
| | 16 | 120 | | var tiposBeneficio = await _context.TiposBeneficio |
| | 16 | 121 | | .AsNoTracking() |
| | 16 | 122 | | .ToDictionaryAsync(t => t.Id); |
| | | 123 | | |
| | 34 | 124 | | foreach (var b in beneficios) |
| | | 125 | | { |
| | 1 | 126 | | if (tiposBeneficio.TryGetValue(b.TipoBeneficioId, out var tipo)) |
| | 0 | 127 | | b.TipoBeneficio = tipo; |
| | | 128 | | } |
| | | 129 | | |
| | | 130 | | // Cargar hijos registrados para beneficios de tipo Asignación Familiar |
| | 16 | 131 | | var idsAsigFamiliar = beneficios |
| | 1 | 132 | | .Where(b => b.TipoBeneficio is not null && |
| | 1 | 133 | | (b.TipoBeneficio.Codigo == "ASIGNACION_FAMILIAR" || |
| | 1 | 134 | | b.TipoBeneficio.Codigo.StartsWith("300") || |
| | 1 | 135 | | b.TipoBeneficio.Nombre.ToUpperInvariant().Contains("ASIGNACION FAMILIAR") || |
| | 1 | 136 | | b.TipoBeneficio.Nombre.ToUpperInvariant().Contains("ASIGNACIÓN FAMILIAR"))) |
| | 0 | 137 | | .Select(b => b.Id) |
| | 16 | 138 | | .ToHashSet(); |
| | | 139 | | |
| | 16 | 140 | | var beneficiariosAsigFam = idsAsigFamiliar.Count > 0 |
| | 16 | 141 | | ? await _context.BeneficioSocialBeneficiarios |
| | 16 | 142 | | .AsNoTracking() |
| | 16 | 143 | | .Where(bh => idsAsigFamiliar.Contains(bh.BeneficioId) && |
| | 16 | 144 | | bh.Activo && |
| | 16 | 145 | | bh.VigenteDesde <= ultimoDiaPeriodo && |
| | 16 | 146 | | (bh.VigenteHasta == null || bh.VigenteHasta >= periodoDate)) |
| | 16 | 147 | | .ToListAsync() |
| | 16 | 148 | | : []; |
| | | 149 | | |
| | 16 | 150 | | var beneficiariosPorBeneficio = beneficiariosAsigFam |
| | 0 | 151 | | .GroupBy(bh => bh.BeneficioId) |
| | 16 | 152 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<BeneficioSocialBeneficiario>)g.ToList()); |
| | | 153 | | |
| | 16 | 154 | | var compensaciones = await _context.ItemsLoteCompensacion |
| | 16 | 155 | | .AsNoTracking() |
| | 16 | 156 | | .Include(i => i.LoteCompensacion) |
| | 16 | 157 | | .ThenInclude(l => l.TipoCompensacion) |
| | 16 | 158 | | .ThenInclude(t => t.Configuraciones) |
| | 16 | 159 | | .ThenInclude(c => c.CatalogoItem) |
| | 16 | 160 | | .Where(i => i.LoteCompensacion.Estado == "aprobado" |
| | 16 | 161 | | && i.LoteCompensacion.PeriodoId == periodoId) |
| | 16 | 162 | | .ToListAsync(); |
| | | 163 | | |
| | 16 | 164 | | var novedadesConfirmadas = await _context.NovedadesPeriodo |
| | 16 | 165 | | .AsNoTracking() |
| | 16 | 166 | | .Where(n => n.PeriodoId == periodoId && n.Estado == "confirmada") |
| | 16 | 167 | | .ToListAsync(); |
| | | 168 | | |
| | 16 | 169 | | var remuneracionesPorGrado = remuneraciones |
| | 37 | 170 | | .Where(r => r.GrupoRemuneracion is null) |
| | 37 | 171 | | .GroupBy(r => r.GradoId) |
| | 16 | 172 | | .ToDictionary( |
| | 17 | 173 | | g => g.Key, |
| | 33 | 174 | | g => (IReadOnlyDictionary<int, RemuneracionGrado>)g |
| | 37 | 175 | | .GroupBy(r => r.Item.Codigo) |
| | 33 | 176 | | .ToDictionary( |
| | 37 | 177 | | gg => gg.Key, |
| | 107 | 178 | | gg => gg.OrderByDescending(r => r.VigenteDesde).First())); |
| | | 179 | | |
| | 16 | 180 | | var remuneracionesPorGradoYGrupo = remuneraciones |
| | 37 | 181 | | .Where(r => r.GrupoRemuneracion is not null) |
| | 0 | 182 | | .GroupBy(r => (r.GradoId, r.Item.Codigo, Grupo: r.GrupoRemuneracion!)) |
| | 16 | 183 | | .ToDictionary( |
| | 0 | 184 | | g => g.Key, |
| | 16 | 185 | | g => g.OrderByDescending(r => r.VigenteDesde).First()); |
| | | 186 | | |
| | 16 | 187 | | var grupoPorEscalafon = escalafones |
| | 16 | 188 | | .ToDictionary( |
| | 16 | 189 | | e => e.Id, |
| | 32 | 190 | | e => GrupoPorCodigoEscalafon.TryGetValue(e.Codigo, out var grupo) ? grupo : null); |
| | | 191 | | |
| | 16 | 192 | | var tarifasPorCategoria = tarifasCategoria |
| | 0 | 193 | | .GroupBy(t => t.Categoria) |
| | 16 | 194 | | .ToDictionary( |
| | 0 | 195 | | g => g.Key, |
| | 16 | 196 | | g => (IReadOnlyDictionary<int, TarifaPorCategoria>)g |
| | 0 | 197 | | .GroupBy(t => t.Item.Codigo) |
| | 16 | 198 | | .ToDictionary( |
| | 0 | 199 | | gg => gg.Key, |
| | 16 | 200 | | gg => gg.OrderByDescending(t => t.VigenteDesde).First())); |
| | | 201 | | |
| | 16 | 202 | | var compPersonal048018PorPersona = compPersonal048018 |
| | 1 | 203 | | .GroupBy(c => c.PersonaId) |
| | 19 | 204 | | .ToDictionary(g => g.Key, g => g.OrderByDescending(c => c.VigenteDesde).First()); |
| | | 205 | | |
| | 16 | 206 | | var permanenciasPorEscalafon = permanencias |
| | 1 | 207 | | .GroupBy(t => t.EscalafonId) |
| | 19 | 208 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<TablaPermanencia>)g.OrderBy(t => t.MesesHasta).ToList()); |
| | | 209 | | |
| | 16 | 210 | | var beneficiosPorPersona = beneficios |
| | 1 | 211 | | .GroupBy(b => b.PersonaId) |
| | 18 | 212 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<BeneficioSocial>)g.ToList()); |
| | | 213 | | |
| | 16 | 214 | | var compensacionesPorPersona = compensaciones |
| | 3 | 215 | | .GroupBy(i => i.PersonaId) |
| | 22 | 216 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<ItemLoteCompensacion>)g.ToList()); |
| | | 217 | | |
| | 16 | 218 | | var novedadesPorPersona = novedadesConfirmadas |
| | 4 | 219 | | .GroupBy(n => n.PersonaId) |
| | 24 | 220 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<NovedadPeriodo>)g.ToList()); |
| | | 221 | | |
| | | 222 | | // Instructivo Asignación Familiar: fila más reciente por tipo_franja |
| | 16 | 223 | | var instructivoAsigFamiliarRaw = await _context.InstructivosAsigFamiliar |
| | 16 | 224 | | .AsNoTracking() |
| | 16 | 225 | | .Where(i => i.VigenteDesde <= periodoDate) |
| | 16 | 226 | | .ToListAsync(); |
| | | 227 | | |
| | 16 | 228 | | var instructivoAsigFamiliar = instructivoAsigFamiliarRaw |
| | 0 | 229 | | .GroupBy(i => i.TipoFranja) |
| | 16 | 230 | | .ToDictionary( |
| | 0 | 231 | | g => g.Key, |
| | 16 | 232 | | g => g.OrderByDescending(i => i.VigenteDesde).First()); |
| | | 233 | | |
| | | 234 | | // Instructivo PSF: fila más reciente por numero_categoria |
| | 16 | 235 | | var instructivoPsfRaw = await _context.InstructivosPsf |
| | 16 | 236 | | .AsNoTracking() |
| | 16 | 237 | | .Where(i => i.VigenteDesde <= periodoDate) |
| | 16 | 238 | | .ToListAsync(); |
| | | 239 | | |
| | 16 | 240 | | var instructivoPsf = instructivoPsfRaw |
| | 0 | 241 | | .GroupBy(i => (int)i.NumeroCategoria) |
| | 16 | 242 | | .ToDictionary( |
| | 0 | 243 | | g => g.Key, |
| | 16 | 244 | | g => g.OrderByDescending(i => i.VigenteDesde).First()); |
| | | 245 | | |
| | | 246 | | // Tabla Hogar: tramos vigentes, ordenados ASC con NULL al final |
| | 16 | 247 | | var tablaPorHogarRaw = await _context.TablasHogarConstituido |
| | 16 | 248 | | .AsNoTracking() |
| | 16 | 249 | | .Where(t => t.VigenteDesde <= periodoDate) |
| | 16 | 250 | | .ToListAsync(); |
| | | 251 | | |
| | 16 | 252 | | var tablaPorHogar = tablaPorHogarRaw |
| | 0 | 253 | | .GroupBy(t => t.HastaHaberes) |
| | 0 | 254 | | .Select(g => g.OrderByDescending(t => t.VigenteDesde).First()) |
| | 0 | 255 | | .OrderBy(t => t.HastaHaberes.HasValue ? 0 : 1) |
| | 0 | 256 | | .ThenBy(t => t.HastaHaberes ?? decimal.MaxValue) |
| | 16 | 257 | | .ToList(); |
| | | 258 | | |
| | 16 | 259 | | var aguinaldos = await _context.Set<Aguinaldo>() |
| | 16 | 260 | | .AsNoTracking() |
| | 16 | 261 | | .Where(a => a.Anio == periodo.Anio) |
| | 16 | 262 | | .ToListAsync(); |
| | | 263 | | |
| | 16 | 264 | | var aguinaldosPorPersona = aguinaldos |
| | 0 | 265 | | .GroupBy(a => a.PersonaId) |
| | 16 | 266 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<Aguinaldo>)g.ToList()); |
| | | 267 | | |
| | 16 | 268 | | var ordenPorGrado = await _context.Grados |
| | 16 | 269 | | .AsNoTracking() |
| | 16 | 270 | | .Where(g => g.Vigente) |
| | 48 | 271 | | .ToDictionaryAsync(g => g.Id, g => g.Orden); |
| | | 272 | | |
| | 16 | 273 | | var diferenciaAscenso = await _context.CompensacionesDiferenciaAscenso |
| | 16 | 274 | | .AsNoTracking() |
| | 16 | 275 | | .Where(c => c.PeriodoId == periodoId && c.Estado == "pendiente") |
| | 17 | 276 | | .ToDictionaryAsync(c => c.PersonaId); |
| | | 277 | | |
| | 16 | 278 | | var descuentosPersonales = await _context.DescuentosPersonalesPeriodo |
| | 16 | 279 | | .AsNoTracking() |
| | 16 | 280 | | .Where(d => d.PeriodoId == periodoId && d.Estado == "aprobado") |
| | 16 | 281 | | .ToListAsync(); |
| | | 282 | | |
| | 16 | 283 | | var descuentosPersonalesPorPersona = descuentosPersonales |
| | 0 | 284 | | .GroupBy(d => d.PersonaId) |
| | 16 | 285 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<DescuentoPersonalPeriodo>)g.ToList()); |
| | | 286 | | |
| | 16 | 287 | | var fictos = await _context.FictosPersona |
| | 16 | 288 | | .AsNoTracking() |
| | 16 | 289 | | .Include(f => f.CatalogoItem) |
| | 16 | 290 | | .Include(f => f.Acreedor) |
| | 16 | 291 | | .Where(f => f.Activo) |
| | 16 | 292 | | .ToListAsync(); |
| | | 293 | | |
| | 16 | 294 | | var fictosPorPersona = fictos |
| | 1 | 295 | | .GroupBy(f => f.PersonaId) |
| | 18 | 296 | | .ToDictionary(g => g.Key, g => (IReadOnlyList<FictoPersona>)g.ToList()); |
| | | 297 | | |
| | 16 | 298 | | return new InsumosPeriodo |
| | 16 | 299 | | { |
| | 16 | 300 | | PeriodoId = periodoId, |
| | 16 | 301 | | ParametroPeriodo = parametros, |
| | 16 | 302 | | CatalogoItems = catalogoItems, |
| | 16 | 303 | | RemuneracionesPorGrado = remuneracionesPorGrado, |
| | 16 | 304 | | RemuneracionesPorGradoYGrupo = remuneracionesPorGradoYGrupo, |
| | 16 | 305 | | GrupoPorEscalafon = grupoPorEscalafon, |
| | 16 | 306 | | TarifasPorCategoria = tarifasPorCategoria, |
| | 16 | 307 | | CompPersonal048018PorPersona = compPersonal048018PorPersona, |
| | 16 | 308 | | PermanenciasPorEscalafon = permanenciasPorEscalafon, |
| | 16 | 309 | | FranjasIrpf = franjasIrpf, |
| | 16 | 310 | | TramosSanidad = tramosSanidad, |
| | 16 | 311 | | BeneficiosPorPersona = beneficiosPorPersona, |
| | 16 | 312 | | CompensacionesPorPersona = compensacionesPorPersona, |
| | 16 | 313 | | NovedadesPorPersona = novedadesPorPersona, |
| | 16 | 314 | | BeneficiariosPorBeneficio = beneficiariosPorBeneficio, |
| | 16 | 315 | | InstructivoAsigFamiliar = instructivoAsigFamiliar, |
| | 16 | 316 | | InstructivoPsf = instructivoPsf, |
| | 16 | 317 | | TablaPorHogar = tablaPorHogar, |
| | 16 | 318 | | AguinaldosPorPersona = aguinaldosPorPersona, |
| | 16 | 319 | | OrdenPorGrado = ordenPorGrado, |
| | 16 | 320 | | CompensacionesDiferenciaAscensoPorPersona = diferenciaAscenso, |
| | 16 | 321 | | DescuentosPersonalesPorPersona = descuentosPersonalesPorPersona, |
| | 16 | 322 | | FictosPorPersona = fictosPorPersona |
| | 16 | 323 | | }; |
| | 16 | 324 | | } |
| | | 325 | | } |