| | | 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 IrpfService : IIrpfService |
| | | 9 | | { |
| | 1 | 10 | | private static readonly HashSet<string> TiposValidos = new(StringComparer.Ordinal) |
| | 1 | 11 | | { |
| | 1 | 12 | | "hijo_menor", "hijo_discapacidad", "conyuge_sin_ingresos" |
| | 1 | 13 | | }; |
| | | 14 | | |
| | | 15 | | private readonly IIrpfRepository _irpfRepository; |
| | | 16 | | private readonly IPersonalRepository _personalRepository; |
| | | 17 | | private readonly ILogger<IrpfService> _logger; |
| | | 18 | | |
| | 15 | 19 | | public IrpfService( |
| | 15 | 20 | | IIrpfRepository irpfRepository, |
| | 15 | 21 | | IPersonalRepository personalRepository, |
| | 15 | 22 | | ILogger<IrpfService> logger) |
| | | 23 | | { |
| | 15 | 24 | | _irpfRepository = irpfRepository; |
| | 15 | 25 | | _personalRepository = personalRepository; |
| | 15 | 26 | | _logger = logger; |
| | 15 | 27 | | } |
| | | 28 | | |
| | | 29 | | // ── Franjas ────────────────────────────────────────────────────────────── |
| | | 30 | | |
| | | 31 | | public async Task<IEnumerable<FranjaIrpfDto>> GetFranjasVigentesAsync(DateTime? fecha = null) |
| | | 32 | | { |
| | 1 | 33 | | var franjas = await _irpfRepository.GetFranjasVigentesAsync(fecha); |
| | 1 | 34 | | return franjas.Select(MapFranjaToDto); |
| | 1 | 35 | | } |
| | | 36 | | |
| | | 37 | | public async Task<FranjaIrpfDto?> GetFranjaByIdAsync(long id) |
| | | 38 | | { |
| | 0 | 39 | | var franja = await _irpfRepository.GetFranjaByIdAsync(id); |
| | 0 | 40 | | return franja == null ? null : MapFranjaToDto(franja); |
| | 0 | 41 | | } |
| | | 42 | | |
| | | 43 | | public async Task<(bool Success, FranjaIrpf? Franja, string? Error)> CreateFranjaAsync(CreateFranjaIrpfRequest reque |
| | | 44 | | { |
| | 6 | 45 | | var (valid, error) = ValidarFranja(request); |
| | 9 | 46 | | if (!valid) return (false, null, error); |
| | | 47 | | |
| | 3 | 48 | | if (await _irpfRepository.ExisteFranjaAsync(request.NumeroFranja, request.VigenteDesde)) |
| | 1 | 49 | | return (false, null, $"Ya existe una franja {request.NumeroFranja} con vigente_desde {request.VigenteDesde:y |
| | | 50 | | |
| | | 51 | | // Cerrar franja anterior del mismo número |
| | 2 | 52 | | var franjaAnterior = await _irpfRepository.GetFranjaActivaByNumeroAsync(request.NumeroFranja); |
| | 2 | 53 | | if (franjaAnterior != null) |
| | | 54 | | { |
| | 1 | 55 | | franjaAnterior.VigenteHasta = request.VigenteDesde.AddDays(-1); |
| | 1 | 56 | | await _irpfRepository.UpdateFranjaAsync(franjaAnterior); |
| | | 57 | | } |
| | | 58 | | |
| | 2 | 59 | | var franja = new FranjaIrpf |
| | 2 | 60 | | { |
| | 2 | 61 | | NumeroFranja = request.NumeroFranja, |
| | 2 | 62 | | DesdeBpc = request.DesdeBpc, |
| | 2 | 63 | | HastaBpc = request.HastaBpc, |
| | 2 | 64 | | Tasa = request.Tasa, |
| | 2 | 65 | | VigenteDesde = request.VigenteDesde, |
| | 2 | 66 | | VigenteHasta = request.VigenteHasta |
| | 2 | 67 | | }; |
| | | 68 | | |
| | 2 | 69 | | var creada = await _irpfRepository.CreateFranjaAsync(franja); |
| | 2 | 70 | | _logger.LogInformation("Franja IRPF {Numero} creada con ID {Id}", franja.NumeroFranja, franja.Id); |
| | 2 | 71 | | return (true, creada, null); |
| | 6 | 72 | | } |
| | | 73 | | |
| | | 74 | | public async Task<(bool Success, FranjaIrpf? Franja, string? Error)> UpdateFranjaAsync(long id, CreateFranjaIrpfRequ |
| | | 75 | | { |
| | 0 | 76 | | var franja = await _irpfRepository.GetFranjaByIdAsync(id); |
| | 0 | 77 | | if (franja == null) return (false, null, "Franja no encontrada"); |
| | | 78 | | |
| | 0 | 79 | | var (valid, error) = ValidarFranja(request); |
| | 0 | 80 | | if (!valid) return (false, null, error); |
| | | 81 | | |
| | | 82 | | // Verificar duplicado solo si cambió el número o la fecha |
| | 0 | 83 | | if (franja.NumeroFranja != request.NumeroFranja || franja.VigenteDesde.Date != request.VigenteDesde.Date) |
| | | 84 | | { |
| | 0 | 85 | | if (await _irpfRepository.ExisteFranjaAsync(request.NumeroFranja, request.VigenteDesde)) |
| | 0 | 86 | | return (false, null, $"Ya existe una franja {request.NumeroFranja} con vigente_desde {request.VigenteDes |
| | | 87 | | } |
| | | 88 | | |
| | 0 | 89 | | franja.NumeroFranja = request.NumeroFranja; |
| | 0 | 90 | | franja.DesdeBpc = request.DesdeBpc; |
| | 0 | 91 | | franja.HastaBpc = request.HastaBpc; |
| | 0 | 92 | | franja.Tasa = request.Tasa; |
| | 0 | 93 | | franja.VigenteDesde = request.VigenteDesde; |
| | 0 | 94 | | franja.VigenteHasta = request.VigenteHasta; |
| | | 95 | | |
| | 0 | 96 | | var actualizada = await _irpfRepository.UpdateFranjaAsync(franja); |
| | 0 | 97 | | return (true, actualizada, null); |
| | 0 | 98 | | } |
| | | 99 | | |
| | | 100 | | public async Task<(bool Success, string? Error)> DeleteFranjaAsync(long id) |
| | | 101 | | { |
| | 1 | 102 | | var franja = await _irpfRepository.GetFranjaByIdAsync(id); |
| | 2 | 103 | | if (franja == null) return (false, "Franja no encontrada"); |
| | | 104 | | |
| | 0 | 105 | | await _irpfRepository.DeleteFranjaAsync(id); |
| | 0 | 106 | | return (true, null); |
| | 1 | 107 | | } |
| | | 108 | | |
| | | 109 | | // ── Deducciones ────────────────────────────────────────────────────────── |
| | | 110 | | |
| | | 111 | | public async Task<PagedResult<DeduccionIrpfDto>> GetDeduccionesPagedAsync( |
| | | 112 | | int page, int pageSize, long? personaId = null, string? tipo = null, bool? activo = null) |
| | | 113 | | { |
| | 0 | 114 | | var (items, totalCount) = await _irpfRepository.GetDeduccionesPagedAsync(page, pageSize, personaId, tipo, activo |
| | 0 | 115 | | return new PagedResult<DeduccionIrpfDto> |
| | 0 | 116 | | { |
| | 0 | 117 | | Items = items.Select(MapDeduccionToDto), |
| | 0 | 118 | | Page = page, |
| | 0 | 119 | | PageSize = pageSize, |
| | 0 | 120 | | TotalCount = totalCount |
| | 0 | 121 | | }; |
| | 0 | 122 | | } |
| | | 123 | | |
| | | 124 | | public async Task<DeduccionIrpfDto?> GetDeduccionByIdAsync(long id) |
| | | 125 | | { |
| | 0 | 126 | | var deduccion = await _irpfRepository.GetDeduccionByIdAsync(id); |
| | 0 | 127 | | return deduccion == null ? null : MapDeduccionToDto(deduccion); |
| | 0 | 128 | | } |
| | | 129 | | |
| | | 130 | | public async Task<(bool Success, DeduccionIrpf? Deduccion, string? Error)> CreateDeduccionAsync(CreateDeduccionIrpfR |
| | | 131 | | { |
| | 5 | 132 | | if (!TiposValidos.Contains(request.Tipo)) |
| | 1 | 133 | | return (false, null, $"Tipo inválido. Valores aceptados: {string.Join(", ", TiposValidos)}"); |
| | | 134 | | |
| | 4 | 135 | | if (request.VigenteHasta.HasValue && request.VigenteHasta.Value < request.VigenteDesde) |
| | 1 | 136 | | return (false, null, "vigente_hasta debe ser mayor o igual a vigente_desde"); |
| | | 137 | | |
| | | 138 | | // Verificar que la persona existe y tiene relación laboral activa |
| | 3 | 139 | | var relacionActiva = await _personalRepository.GetActivoByPersonaIdAsync(request.PersonaId); |
| | 3 | 140 | | if (relacionActiva == null) |
| | 1 | 141 | | return (false, null, "No se encontró personal con ese ID o no tiene relación laboral activa"); |
| | | 142 | | |
| | | 143 | | // Regla: no duplicar cónyuge activo |
| | 2 | 144 | | if (request.Tipo == "conyuge_sin_ingresos") |
| | | 145 | | { |
| | 1 | 146 | | if (await _irpfRepository.ExisteDeduccionActivaAsync(request.PersonaId, "conyuge_sin_ingresos")) |
| | 1 | 147 | | return (false, null, "Ya existe una deducción activa de tipo conyuge_sin_ingresos para este funcionario" |
| | | 148 | | } |
| | | 149 | | |
| | 1 | 150 | | var deduccion = new DeduccionIrpf |
| | 1 | 151 | | { |
| | 1 | 152 | | PersonaId = request.PersonaId, |
| | 1 | 153 | | Tipo = request.Tipo, |
| | 1 | 154 | | Descripcion = request.Descripcion, |
| | 1 | 155 | | VigenteDesde = request.VigenteDesde, |
| | 1 | 156 | | VigenteHasta = request.VigenteHasta, |
| | 1 | 157 | | Activo = true |
| | 1 | 158 | | }; |
| | | 159 | | |
| | 1 | 160 | | var creada = await _irpfRepository.CreateDeduccionAsync(deduccion); |
| | 1 | 161 | | _logger.LogInformation("Deducción IRPF tipo {Tipo} creada para persona {PersonaId}", deduccion.Tipo, deduccion.P |
| | 1 | 162 | | return (true, creada, null); |
| | 5 | 163 | | } |
| | | 164 | | |
| | | 165 | | public async Task<(bool Success, DeduccionIrpf? Deduccion, string? Error)> UpdateDeduccionAsync(long id, UpdateDeduc |
| | | 166 | | { |
| | 2 | 167 | | var deduccion = await _irpfRepository.GetDeduccionByIdAsync(id); |
| | 3 | 168 | | if (deduccion == null) return (false, null, "Deducción no encontrada"); |
| | | 169 | | |
| | 1 | 170 | | if (request.Descripcion != null) |
| | 0 | 171 | | deduccion.Descripcion = request.Descripcion; |
| | | 172 | | |
| | 1 | 173 | | if (request.VigenteHasta.HasValue) |
| | | 174 | | { |
| | 0 | 175 | | if (request.VigenteHasta.Value < deduccion.VigenteDesde) |
| | 0 | 176 | | return (false, null, "vigente_hasta debe ser mayor o igual a vigente_desde"); |
| | 0 | 177 | | deduccion.VigenteHasta = request.VigenteHasta; |
| | | 178 | | } |
| | | 179 | | |
| | 1 | 180 | | if (request.Activo.HasValue) |
| | | 181 | | { |
| | 1 | 182 | | deduccion.Activo = request.Activo.Value; |
| | | 183 | | // Al desactivar sin fecha de cierre, cerrar con hoy |
| | 1 | 184 | | if (!request.Activo.Value && !deduccion.VigenteHasta.HasValue && !request.VigenteHasta.HasValue) |
| | 1 | 185 | | deduccion.VigenteHasta = DateTime.UtcNow.Date; |
| | | 186 | | } |
| | | 187 | | |
| | 1 | 188 | | var actualizada = await _irpfRepository.UpdateDeduccionAsync(deduccion); |
| | 1 | 189 | | return (true, actualizada, null); |
| | 2 | 190 | | } |
| | | 191 | | |
| | | 192 | | // ── Helpers ────────────────────────────────────────────────────────────── |
| | | 193 | | |
| | | 194 | | private static (bool Valid, string? Error) ValidarFranja(CreateFranjaIrpfRequest r) |
| | | 195 | | { |
| | 6 | 196 | | if (r.NumeroFranja == 1 && r.DesdeBpc != 0) |
| | 1 | 197 | | return (false, "La franja 1 debe tener desdeBpc = 0"); |
| | | 198 | | |
| | 5 | 199 | | if (r.NumeroFranja == 7 && r.HastaBpc.HasValue) |
| | 1 | 200 | | return (false, "La última franja (7) no puede tener límite superior (hastaBpc debe ser null)"); |
| | | 201 | | |
| | 4 | 202 | | if (r.HastaBpc.HasValue && r.DesdeBpc >= r.HastaBpc.Value) |
| | 1 | 203 | | return (false, "desdeBpc debe ser menor que hastaBpc"); |
| | | 204 | | |
| | 3 | 205 | | return (true, null); |
| | | 206 | | } |
| | | 207 | | |
| | 2 | 208 | | private static FranjaIrpfDto MapFranjaToDto(FranjaIrpf f) => new() |
| | 2 | 209 | | { |
| | 2 | 210 | | Id = f.Id, |
| | 2 | 211 | | NumeroFranja = f.NumeroFranja, |
| | 2 | 212 | | DesdeBpc = f.DesdeBpc, |
| | 2 | 213 | | HastaBpc = f.HastaBpc, |
| | 2 | 214 | | Tasa = f.Tasa, |
| | 2 | 215 | | VigenteDesde = f.VigenteDesde, |
| | 2 | 216 | | VigenteHasta = f.VigenteHasta |
| | 2 | 217 | | }; |
| | | 218 | | |
| | 0 | 219 | | private static DeduccionIrpfDto MapDeduccionToDto(DeduccionIrpf d) => new() |
| | 0 | 220 | | { |
| | 0 | 221 | | Id = d.Id, |
| | 0 | 222 | | PersonaId = d.PersonaId, |
| | 0 | 223 | | NombrePersona = d.Persona?.NombreCompleto ?? string.Empty, |
| | 0 | 224 | | Cedula = d.Persona?.Cedula ?? string.Empty, |
| | 0 | 225 | | Tipo = d.Tipo, |
| | 0 | 226 | | Descripcion = d.Descripcion, |
| | 0 | 227 | | VigenteDesde = d.VigenteDesde, |
| | 0 | 228 | | VigenteHasta = d.VigenteHasta, |
| | 0 | 229 | | Activo = d.Activo |
| | 0 | 230 | | }; |
| | | 231 | | } |