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