| | | 1 | | using FAU.DataAccess.Repositories; |
| | | 2 | | using FAU.Entidades; |
| | | 3 | | using FAU.Logica.DTOs; |
| | | 4 | | using Microsoft.Extensions.Logging; |
| | | 5 | | |
| | | 6 | | namespace FAU.Logica.Services; |
| | | 7 | | |
| | | 8 | | public class ParametroLiquidacionService : IParametroLiquidacionService |
| | | 9 | | { |
| | | 10 | | private readonly IParametroLiquidacionRepository _parametroRepository; |
| | | 11 | | private readonly IIrpfRepository _irpfRepository; |
| | | 12 | | private readonly IAuditoriaService _auditoriaService; |
| | | 13 | | private readonly ICurrentUserContext _currentUser; |
| | | 14 | | private readonly ILogger<ParametroLiquidacionService> _logger; |
| | | 15 | | |
| | 6 | 16 | | public ParametroLiquidacionService( |
| | 6 | 17 | | IParametroLiquidacionRepository parametroRepository, |
| | 6 | 18 | | IIrpfRepository irpfRepository, |
| | 6 | 19 | | IAuditoriaService auditoriaService, |
| | 6 | 20 | | ICurrentUserContext currentUser, |
| | 6 | 21 | | ILogger<ParametroLiquidacionService> logger) |
| | | 22 | | { |
| | 6 | 23 | | _parametroRepository = parametroRepository; |
| | 6 | 24 | | _irpfRepository = irpfRepository; |
| | 6 | 25 | | _auditoriaService = auditoriaService; |
| | 6 | 26 | | _currentUser = currentUser; |
| | 6 | 27 | | _logger = logger; |
| | 6 | 28 | | } |
| | | 29 | | |
| | | 30 | | public async Task<ParametroLiquidacionDto?> ObtenerActualAsync() |
| | | 31 | | { |
| | 1 | 32 | | var parametro = await _parametroRepository.ObtenerActualAsync(); |
| | 1 | 33 | | return parametro == null ? null : MapToDto(parametro); |
| | 1 | 34 | | } |
| | | 35 | | |
| | | 36 | | public async Task<(bool Success, ParametroLiquidacionDto? Parametro, string? Error)> GuardarAsync( |
| | | 37 | | long usuarioId, ConfigurarParametrosLiquidacionRequest request) |
| | | 38 | | { |
| | 3 | 39 | | var error = ValidarParametro(request); |
| | 3 | 40 | | if (error != null) |
| | 1 | 41 | | return (false, null, error); |
| | | 42 | | |
| | 2 | 43 | | var existente = await _parametroRepository.ObtenerActualAsync(); |
| | | 44 | | |
| | | 45 | | ParametroPeriodo resultado; |
| | 2 | 46 | | if (existente != null) |
| | | 47 | | { |
| | 1 | 48 | | existente.BpcAnual = request.BpcAnual; |
| | 1 | 49 | | existente.TasaMontepioLeyVieja = request.TasaMontepioLeyVieja; |
| | 1 | 50 | | existente.TasaMontepioLeyNueva = request.TasaMontepioLeyNueva; |
| | 1 | 51 | | existente.TasaFonasa = request.TasaFonasa; |
| | 1 | 52 | | existente.TasaFrl = request.TasaFrl; |
| | 1 | 53 | | existente.UmbralDeduccionBpc = request.UmbralDeduccionBpc; |
| | 1 | 54 | | existente.TasaDeduccionAlta = request.TasaDeduccionAlta; |
| | 1 | 55 | | existente.TasaDeduccionBaja = request.TasaDeduccionBaja; |
| | 1 | 56 | | existente.PorcentajeMinimoDeficit = request.PorcentajeMinimoDeficit; |
| | 1 | 57 | | existente.CreadoPor = usuarioId; |
| | 1 | 58 | | existente.CreadoEn = DateTime.Now; |
| | 1 | 59 | | resultado = await _parametroRepository.ActualizarAsync(existente); |
| | | 60 | | } |
| | | 61 | | else |
| | | 62 | | { |
| | 1 | 63 | | var nuevo = new ParametroPeriodo |
| | 1 | 64 | | { |
| | 1 | 65 | | Anio = (short)DateTime.Now.Year, |
| | 1 | 66 | | Mes = (short)DateTime.Now.Month, |
| | 1 | 67 | | BpcAnual = request.BpcAnual, |
| | 1 | 68 | | TasaMontepioLeyVieja = request.TasaMontepioLeyVieja, |
| | 1 | 69 | | TasaMontepioLeyNueva = request.TasaMontepioLeyNueva, |
| | 1 | 70 | | TasaFonasa = request.TasaFonasa, |
| | 1 | 71 | | TasaFrl = request.TasaFrl, |
| | 1 | 72 | | UmbralDeduccionBpc = request.UmbralDeduccionBpc, |
| | 1 | 73 | | TasaDeduccionAlta = request.TasaDeduccionAlta, |
| | 1 | 74 | | TasaDeduccionBaja = request.TasaDeduccionBaja, |
| | 1 | 75 | | PorcentajeMinimoDeficit = request.PorcentajeMinimoDeficit, |
| | 1 | 76 | | CreadoPor = usuarioId, |
| | 1 | 77 | | CreadoEn = DateTime.Now |
| | 1 | 78 | | }; |
| | 1 | 79 | | resultado = await _parametroRepository.CrearAsync(nuevo); |
| | | 80 | | } |
| | | 81 | | |
| | 2 | 82 | | _logger.LogInformation("Parámetros de liquidación actualizados por usuario {UsuarioId}", usuarioId); |
| | 2 | 83 | | await _auditoriaService.LogAuditoriaAsync( |
| | 2 | 84 | | usuarioId, AccionEnum.ActualizarCompensacion, ContextoEnum.Liquidaciones, |
| | 2 | 85 | | _currentUser.Host, entidad: "ParametroPeriodo", entidadId: resultado.Id, |
| | 2 | 86 | | detalle: new { bpcAnual = resultado.BpcAnual, tasaFonasa = resultado.TasaFonasa }); |
| | | 87 | | |
| | 2 | 88 | | return (true, MapToDto(resultado), null); |
| | 3 | 89 | | } |
| | | 90 | | |
| | | 91 | | public async Task<ConfiguracionFranjasIrpfDto?> ObtenerFranjasAsync() |
| | | 92 | | { |
| | 0 | 93 | | var franjas = (await _irpfRepository.GetFranjasVigentesAsync(null)).ToList(); |
| | 0 | 94 | | return franjas.Count == 0 ? null : MapFranjas(franjas); |
| | 0 | 95 | | } |
| | | 96 | | |
| | | 97 | | public async Task<(bool Success, ConfiguracionFranjasIrpfDto? Configuracion, string? Error)> GuardarFranjasAsync( |
| | | 98 | | ConfigurarFranjasIrpfRequest request) |
| | | 99 | | { |
| | 2 | 100 | | if (request.Franjas == null || request.Franjas.Count == 0) |
| | 0 | 101 | | return (false, null, "Debe proporcionar al menos una franja IRPF"); |
| | | 102 | | |
| | 6 | 103 | | var franjasOrdenadas = request.Franjas.OrderBy(f => f.NumeroFranja).ToList(); |
| | | 104 | | |
| | 2 | 105 | | var error = ValidarCoberturaFranjas(franjasOrdenadas); |
| | 2 | 106 | | if (error != null) |
| | 1 | 107 | | return (false, null, error); |
| | | 108 | | |
| | | 109 | | // Eliminar todas las franjas existentes |
| | 1 | 110 | | var existentes = (await _irpfRepository.GetHistorialFranjasAsync()).ToList(); |
| | 6 | 111 | | foreach (var franja in existentes) |
| | 2 | 112 | | await _irpfRepository.DeleteFranjaAsync(franja.Id); |
| | | 113 | | |
| | | 114 | | // Crear las nuevas |
| | 3 | 115 | | var nuevasFranjas = franjasOrdenadas.Select(f => new FranjaIrpf |
| | 3 | 116 | | { |
| | 3 | 117 | | NumeroFranja = f.NumeroFranja, |
| | 3 | 118 | | DesdeBpc = f.DesdeBpc, |
| | 3 | 119 | | HastaBpc = f.HastaBpc, |
| | 3 | 120 | | Tasa = f.Tasa, |
| | 3 | 121 | | VigenteDesde = DateTime.Now.Date |
| | 3 | 122 | | }).ToList(); |
| | | 123 | | |
| | 1 | 124 | | await _irpfRepository.CreateFranjasAsync(nuevasFranjas); |
| | 1 | 125 | | _logger.LogInformation("Franjas IRPF reemplazadas: {Count} franjas", nuevasFranjas.Count); |
| | | 126 | | |
| | 1 | 127 | | await _auditoriaService.LogAuditoriaAsync( |
| | 1 | 128 | | _currentUser.UserId ?? 0, AccionEnum.ActualizarFranja, ContextoEnum.IRPF, |
| | 1 | 129 | | _currentUser.Host, entidad: "FranjaIrpf", entidadId: 0, |
| | 1 | 130 | | detalle: new { cantidadFranjas = nuevasFranjas.Count }); |
| | | 131 | | |
| | 1 | 132 | | return (true, MapFranjas(nuevasFranjas), null); |
| | 2 | 133 | | } |
| | | 134 | | |
| | | 135 | | // ── Validaciones ──────────────────────────────────────────────────────── |
| | | 136 | | |
| | | 137 | | private static string? ValidarParametro(ConfigurarParametrosLiquidacionRequest request) |
| | | 138 | | { |
| | 3 | 139 | | if (request.BpcAnual <= 0) |
| | 1 | 140 | | return "La BPC anual debe ser mayor a cero"; |
| | | 141 | | |
| | 2 | 142 | | var tasas = new Dictionary<string, decimal> |
| | 2 | 143 | | { |
| | 2 | 144 | | ["tasa_montepio_ley_vieja"] = request.TasaMontepioLeyVieja, |
| | 2 | 145 | | ["tasa_montepio_ley_nueva"] = request.TasaMontepioLeyNueva, |
| | 2 | 146 | | ["tasa_fonasa"] = request.TasaFonasa, |
| | 2 | 147 | | ["tasa_frl"] = request.TasaFrl, |
| | 2 | 148 | | ["tasa_deduccion_alta"] = request.TasaDeduccionAlta, |
| | 2 | 149 | | ["tasa_deduccion_baja"] = request.TasaDeduccionBaja, |
| | 2 | 150 | | ["porcentaje_minimo_deficit"] = request.PorcentajeMinimoDeficit |
| | 2 | 151 | | }; |
| | | 152 | | |
| | 32 | 153 | | foreach (var tasa in tasas) |
| | | 154 | | { |
| | 14 | 155 | | if (tasa.Value < 0 || tasa.Value > 1) |
| | 0 | 156 | | return $"El valor de {tasa.Key} debe estar entre 0 y 1"; |
| | | 157 | | } |
| | | 158 | | |
| | 2 | 159 | | if (request.UmbralDeduccionBpc < 0) |
| | 0 | 160 | | return "El umbral de deducción en BPC no puede ser negativo"; |
| | | 161 | | |
| | 2 | 162 | | return null; |
| | 0 | 163 | | } |
| | | 164 | | |
| | | 165 | | private static string? ValidarCoberturaFranjas(IReadOnlyList<FranjaIrpfConfiguradaRequest> franjas) |
| | | 166 | | { |
| | 10 | 167 | | var duplicados = franjas.GroupBy(f => f.NumeroFranja).FirstOrDefault(g => g.Count() > 1); |
| | 2 | 168 | | if (duplicados != null) |
| | 0 | 169 | | return $"La franja {duplicados.Key} está duplicada"; |
| | | 170 | | |
| | 2 | 171 | | if (franjas[0].DesdeBpc != 0) |
| | 0 | 172 | | return "La primera franja debe comenzar en 0 BPC"; |
| | | 173 | | |
| | 8 | 174 | | for (var i = 0; i < franjas.Count; i++) |
| | | 175 | | { |
| | 3 | 176 | | var franja = franjas[i]; |
| | | 177 | | |
| | 3 | 178 | | if (franja.HastaBpc.HasValue && franja.DesdeBpc >= franja.HastaBpc.Value) |
| | 0 | 179 | | return $"La franja {franja.NumeroFranja} tiene un rango inválido"; |
| | | 180 | | |
| | 3 | 181 | | if (franja.Tasa < 0 || franja.Tasa > 1) |
| | 0 | 182 | | return $"La tasa de la franja {franja.NumeroFranja} debe estar entre 0 y 1"; |
| | | 183 | | |
| | 3 | 184 | | if (i < franjas.Count - 1) |
| | | 185 | | { |
| | 2 | 186 | | var siguiente = franjas[i + 1]; |
| | 2 | 187 | | if (!franja.HastaBpc.HasValue) |
| | 0 | 188 | | return $"La franja {franja.NumeroFranja} solo puede quedar abierta si es la última"; |
| | 2 | 189 | | if (franja.HastaBpc.Value != siguiente.DesdeBpc) |
| | 1 | 190 | | return $"Las franjas {franja.NumeroFranja} y {siguiente.NumeroFranja} no cubren el rango de forma co |
| | | 191 | | } |
| | 1 | 192 | | else if (franja.HastaBpc.HasValue) |
| | | 193 | | { |
| | 0 | 194 | | return "La última franja debe quedar abierta sin límite superior"; |
| | | 195 | | } |
| | | 196 | | } |
| | | 197 | | |
| | 1 | 198 | | return null; |
| | | 199 | | } |
| | | 200 | | |
| | | 201 | | // ── Mappers ───────────────────────────────────────────────────────────── |
| | | 202 | | |
| | 3 | 203 | | private static ParametroLiquidacionDto MapToDto(ParametroPeriodo p) => new() |
| | 3 | 204 | | { |
| | 3 | 205 | | Id = p.Id, |
| | 3 | 206 | | Anio = p.Anio, |
| | 3 | 207 | | Mes = p.Mes, |
| | 3 | 208 | | BpcAnual = p.BpcAnual, |
| | 3 | 209 | | TasaMontepioLeyVieja = p.TasaMontepioLeyVieja, |
| | 3 | 210 | | TasaMontepioLeyNueva = p.TasaMontepioLeyNueva, |
| | 3 | 211 | | TasaFonasa = p.TasaFonasa, |
| | 3 | 212 | | TasaFrl = p.TasaFrl, |
| | 3 | 213 | | UmbralDeduccionBpc = p.UmbralDeduccionBpc, |
| | 3 | 214 | | TasaDeduccionAlta = p.TasaDeduccionAlta, |
| | 3 | 215 | | TasaDeduccionBaja = p.TasaDeduccionBaja, |
| | 3 | 216 | | PorcentajeMinimoDeficit = p.PorcentajeMinimoDeficit, |
| | 3 | 217 | | CreadoEn = p.CreadoEn, |
| | 3 | 218 | | CreadoPor = p.CreadoPor, |
| | 3 | 219 | | UsuarioCreador = p.UsuarioCreador?.Username |
| | 3 | 220 | | }; |
| | | 221 | | |
| | | 222 | | private static ConfiguracionFranjasIrpfDto MapFranjas(IEnumerable<FranjaIrpf> franjas) |
| | | 223 | | { |
| | 3 | 224 | | var lista = franjas.OrderBy(f => f.NumeroFranja).ToList(); |
| | 1 | 225 | | return new ConfiguracionFranjasIrpfDto |
| | 1 | 226 | | { |
| | 1 | 227 | | FechaVigencia = lista[0].VigenteDesde, |
| | 1 | 228 | | FechaFinVigencia = lista[0].VigenteHasta, |
| | 2 | 229 | | Franjas = lista.Select(f => new FranjaIrpfDto |
| | 2 | 230 | | { |
| | 2 | 231 | | Id = f.Id, |
| | 2 | 232 | | NumeroFranja = f.NumeroFranja, |
| | 2 | 233 | | DesdeBpc = f.DesdeBpc, |
| | 2 | 234 | | HastaBpc = f.HastaBpc, |
| | 2 | 235 | | Tasa = f.Tasa, |
| | 2 | 236 | | VigenteDesde = f.VigenteDesde, |
| | 2 | 237 | | VigenteHasta = f.VigenteHasta |
| | 2 | 238 | | }).ToList() |
| | 1 | 239 | | }; |
| | | 240 | | } |
| | | 241 | | } |