| | | 1 | | using System.Text; |
| | | 2 | | using System.Text.RegularExpressions; |
| | | 3 | | using FAU.DataAccess; |
| | | 4 | | using FAU.DataAccess.Repositories; |
| | | 5 | | using FAU.Entidades; |
| | | 6 | | using FAU.Logica.DTOs; |
| | | 7 | | using FAU.Logica.Validadores; |
| | | 8 | | using Microsoft.EntityFrameworkCore; |
| | | 9 | | using Microsoft.EntityFrameworkCore.Storage; |
| | | 10 | | |
| | | 11 | | namespace FAU.Logica.Services; |
| | | 12 | | |
| | | 13 | | public class FictoPersonaService : IFictoPersonaService |
| | | 14 | | { |
| | | 15 | | private const string CodigoAcreedorMdn = "MDN"; |
| | 1 | 16 | | private static readonly IReadOnlyDictionary<string, int> CodigoPorTipo = |
| | 1 | 17 | | new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) |
| | 1 | 18 | | { |
| | 1 | 19 | | [TiposFicto.Comedor] = 608, |
| | 1 | 20 | | [TiposFicto.Guarderia] = 614, |
| | 1 | 21 | | [TiposFicto.Ticket] = 618 |
| | 1 | 22 | | }; |
| | | 23 | | |
| | 1 | 24 | | private static readonly IReadOnlyDictionary<string, int> CodigoEspejoPorTipo = |
| | 1 | 25 | | new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) |
| | 1 | 26 | | { |
| | 1 | 27 | | [TiposFicto.Comedor] = 372, |
| | 1 | 28 | | [TiposFicto.Guarderia] = 375, |
| | 1 | 29 | | [TiposFicto.Ticket] = 376 |
| | 1 | 30 | | }; |
| | | 31 | | |
| | 1 | 32 | | private static readonly IReadOnlyDictionary<int, string> TipoPorCodigo = |
| | 7 | 33 | | CodigoPorTipo.ToDictionary(par => par.Value, par => par.Key); |
| | | 34 | | |
| | | 35 | | private readonly IFictoPersonaRepository _repositorio; |
| | | 36 | | private readonly IPersonalRepository _repositorioPersonal; |
| | | 37 | | private readonly ApplicationDbContext _contexto; |
| | | 38 | | private readonly IAuditoriaService _auditoria; |
| | | 39 | | |
| | 14 | 40 | | public FictoPersonaService( |
| | 14 | 41 | | IFictoPersonaRepository repositorio, |
| | 14 | 42 | | IPersonalRepository repositorioPersonal, |
| | 14 | 43 | | ApplicationDbContext contexto, |
| | 14 | 44 | | IAuditoriaService auditoria) |
| | | 45 | | { |
| | 14 | 46 | | _repositorio = repositorio; |
| | 14 | 47 | | _repositorioPersonal = repositorioPersonal; |
| | 14 | 48 | | _contexto = contexto; |
| | 14 | 49 | | _auditoria = auditoria; |
| | 14 | 50 | | } |
| | | 51 | | |
| | | 52 | | public async Task<PagedResult<FictoPersonaDto>> ObtenerPaginadoAsync( |
| | | 53 | | int pagina, |
| | | 54 | | int tamano, |
| | | 55 | | string? busqueda, |
| | | 56 | | string? tipo, |
| | | 57 | | bool? activo) |
| | | 58 | | { |
| | 0 | 59 | | pagina = Math.Max(1, pagina); |
| | 0 | 60 | | tamano = Math.Clamp(tamano, 1, 100); |
| | | 61 | | |
| | 0 | 62 | | long? catalogoItemId = null; |
| | 0 | 63 | | if (!string.IsNullOrWhiteSpace(tipo)) |
| | 0 | 64 | | catalogoItemId = (await ResolverCatalogoAsync(tipo)).Id; |
| | | 65 | | |
| | 0 | 66 | | var (items, total) = await _repositorio.ObtenerPaginadoAsync( |
| | 0 | 67 | | pagina, |
| | 0 | 68 | | tamano, |
| | 0 | 69 | | busqueda, |
| | 0 | 70 | | catalogoItemId, |
| | 0 | 71 | | activo); |
| | | 72 | | |
| | 0 | 73 | | return new PagedResult<FictoPersonaDto> |
| | 0 | 74 | | { |
| | 0 | 75 | | Items = items.Select(MapearDto), |
| | 0 | 76 | | Page = pagina, |
| | 0 | 77 | | PageSize = tamano, |
| | 0 | 78 | | TotalCount = total |
| | 0 | 79 | | }; |
| | 0 | 80 | | } |
| | | 81 | | |
| | | 82 | | public async Task<FictoPersonaDto?> ObtenerPorIdAsync(long id) |
| | | 83 | | { |
| | 1 | 84 | | var ficto = await _repositorio.ObtenerPorIdAsync(id); |
| | 1 | 85 | | return ficto is null ? null : MapearDto(ficto); |
| | 1 | 86 | | } |
| | | 87 | | |
| | | 88 | | public async Task<FictoPersonaDto> CrearAsync( |
| | | 89 | | CrearFictoPersonaRequest request, |
| | | 90 | | long usuarioId, |
| | | 91 | | string? host) |
| | | 92 | | { |
| | 10 | 93 | | ValidarImporte(request.Importe); |
| | 10 | 94 | | var cedula = NormalizarCedula(request.CedulaTitular); |
| | 10 | 95 | | var relacion = await _repositorioPersonal.GetByCedulaAsync(cedula) |
| | 10 | 96 | | ?? throw new ArgumentException($"No se encontró personal activo con cédula {cedula}."); |
| | 10 | 97 | | var catalogo = await ResolverCatalogoAsync(request.Tipo); |
| | 9 | 98 | | var acreedor = await ResolverMdnAsync(); |
| | | 99 | | |
| | 8 | 100 | | var existente = await _repositorio.ObtenerPorPersonaYCatalogoAsync( |
| | 8 | 101 | | relacion.PersonaId, |
| | 8 | 102 | | catalogo.Id); |
| | 8 | 103 | | if (existente is not null) |
| | | 104 | | { |
| | 1 | 105 | | var estado = existente.Activo ? "activo" : "inactivo"; |
| | 1 | 106 | | throw new InvalidOperationException( |
| | 1 | 107 | | $"La persona ya tiene un ficto de tipo {NormalizarTipo(request.Tipo)} {estado}."); |
| | | 108 | | } |
| | | 109 | | |
| | 7 | 110 | | var ficto = new FictoPersona |
| | 7 | 111 | | { |
| | 7 | 112 | | PersonaId = relacion.PersonaId, |
| | 7 | 113 | | AcreedorId = acreedor.Id, |
| | 7 | 114 | | CatalogoItemId = catalogo.Id, |
| | 7 | 115 | | Importe = RedondearImporte(request.Importe), |
| | 7 | 116 | | Activo = true, |
| | 7 | 117 | | Observaciones = NormalizarObservaciones(request.Observaciones), |
| | 7 | 118 | | FechaCreacion = DateTime.UtcNow, |
| | 7 | 119 | | UsuarioCreacionId = UsuarioValido(usuarioId) |
| | 7 | 120 | | }; |
| | | 121 | | |
| | 7 | 122 | | await _repositorio.CrearAsync(ficto); |
| | 7 | 123 | | await _auditoria.LogAuditoriaAsync( |
| | 7 | 124 | | usuarioId, |
| | 7 | 125 | | AccionEnum.CrearFicto, |
| | 7 | 126 | | ContextoEnum.Descuentos, |
| | 7 | 127 | | host, |
| | 7 | 128 | | nameof(FictoPersona), |
| | 7 | 129 | | ficto.Id, |
| | 7 | 130 | | new |
| | 7 | 131 | | { |
| | 7 | 132 | | ficto.PersonaId, |
| | 7 | 133 | | Cedula = cedula, |
| | 7 | 134 | | Tipo = NormalizarTipo(request.Tipo), |
| | 7 | 135 | | ficto.Importe, |
| | 7 | 136 | | Acreedor = CodigoAcreedorMdn |
| | 7 | 137 | | }); |
| | | 138 | | |
| | 7 | 139 | | var fictoCompleto = await _repositorio.ObtenerPorIdAsync(ficto.Id) |
| | 7 | 140 | | ?? throw new InvalidOperationException("No fue posible recuperar el ficto creado."); |
| | 7 | 141 | | return MapearDto(fictoCompleto); |
| | 7 | 142 | | } |
| | | 143 | | |
| | | 144 | | public async Task<FictoPersonaDto> ActualizarAsync( |
| | | 145 | | long id, |
| | | 146 | | ActualizarFictoPersonaRequest request, |
| | | 147 | | long usuarioId, |
| | | 148 | | string? host) |
| | | 149 | | { |
| | 2 | 150 | | ValidarImporte(request.Importe); |
| | 2 | 151 | | var ficto = await _repositorio.ObtenerPorIdAsync(id) |
| | 2 | 152 | | ?? throw new ArgumentException($"Ficto {id} no encontrado."); |
| | 1 | 153 | | if (!ficto.Activo) |
| | 0 | 154 | | throw new InvalidOperationException("No se puede editar un ficto inactivo. Debe reactivarlo primero."); |
| | | 155 | | |
| | 1 | 156 | | var anterior = new |
| | 1 | 157 | | { |
| | 1 | 158 | | ficto.Importe, |
| | 1 | 159 | | ficto.Observaciones |
| | 1 | 160 | | }; |
| | | 161 | | |
| | 1 | 162 | | ficto.Importe = RedondearImporte(request.Importe); |
| | 1 | 163 | | ficto.Observaciones = NormalizarObservaciones(request.Observaciones); |
| | 1 | 164 | | ficto.FechaModificacion = DateTime.UtcNow; |
| | 1 | 165 | | ficto.UsuarioModificacionId = UsuarioValido(usuarioId); |
| | 1 | 166 | | await _repositorio.ActualizarAsync(ficto); |
| | | 167 | | |
| | 1 | 168 | | await _auditoria.LogAuditoriaAsync( |
| | 1 | 169 | | usuarioId, |
| | 1 | 170 | | AccionEnum.ActualizarFicto, |
| | 1 | 171 | | ContextoEnum.Descuentos, |
| | 1 | 172 | | host, |
| | 1 | 173 | | nameof(FictoPersona), |
| | 1 | 174 | | ficto.Id, |
| | 1 | 175 | | new |
| | 1 | 176 | | { |
| | 1 | 177 | | Antes = anterior, |
| | 1 | 178 | | Despues = new { ficto.Importe, ficto.Observaciones } |
| | 1 | 179 | | }); |
| | | 180 | | |
| | 1 | 181 | | return MapearDto(ficto); |
| | 1 | 182 | | } |
| | | 183 | | |
| | | 184 | | public async Task DarBajaAsync(long id, long usuarioId, string? host) |
| | | 185 | | { |
| | 2 | 186 | | var ficto = await _repositorio.ObtenerPorIdAsync(id) |
| | 2 | 187 | | ?? throw new ArgumentException($"Ficto {id} no encontrado."); |
| | 1 | 188 | | if (!ficto.Activo) |
| | 0 | 189 | | throw new InvalidOperationException("El ficto ya está inactivo."); |
| | | 190 | | |
| | 1 | 191 | | ficto.Activo = false; |
| | 1 | 192 | | ficto.FechaBaja = DateTime.UtcNow; |
| | 1 | 193 | | ficto.FechaModificacion = ficto.FechaBaja; |
| | 1 | 194 | | ficto.UsuarioBajaId = UsuarioValido(usuarioId); |
| | 1 | 195 | | ficto.UsuarioModificacionId = UsuarioValido(usuarioId); |
| | 1 | 196 | | await _repositorio.ActualizarAsync(ficto); |
| | | 197 | | |
| | 1 | 198 | | await _auditoria.LogAuditoriaAsync( |
| | 1 | 199 | | usuarioId, |
| | 1 | 200 | | AccionEnum.DarBajaFicto, |
| | 1 | 201 | | ContextoEnum.Descuentos, |
| | 1 | 202 | | host, |
| | 1 | 203 | | nameof(FictoPersona), |
| | 1 | 204 | | ficto.Id, |
| | 1 | 205 | | new |
| | 1 | 206 | | { |
| | 1 | 207 | | ficto.PersonaId, |
| | 1 | 208 | | ficto.CatalogoItemId, |
| | 1 | 209 | | ficto.Importe, |
| | 1 | 210 | | ficto.FechaBaja |
| | 1 | 211 | | }); |
| | 1 | 212 | | } |
| | | 213 | | |
| | | 214 | | public async Task<FictoPersonaDto> ReactivarAsync(long id, long usuarioId, string? host) |
| | | 215 | | { |
| | 2 | 216 | | var ficto = await _repositorio.ObtenerPorIdAsync(id) |
| | 2 | 217 | | ?? throw new ArgumentException($"Ficto {id} no encontrado."); |
| | 1 | 218 | | if (ficto.Activo) |
| | 0 | 219 | | throw new InvalidOperationException("El ficto ya está activo."); |
| | | 220 | | |
| | 1 | 221 | | var personalActivo = await _contexto.RelacionesLaborales |
| | 1 | 222 | | .AnyAsync(r => r.PersonaId == ficto.PersonaId && r.Estado == EstadoRelacion.Activo); |
| | 1 | 223 | | if (!personalActivo) |
| | 0 | 224 | | throw new InvalidOperationException("No se puede reactivar el ficto porque la persona no tiene una relación |
| | | 225 | | |
| | 1 | 226 | | ficto.Activo = true; |
| | 1 | 227 | | ficto.FechaBaja = null; |
| | 1 | 228 | | ficto.UsuarioBajaId = null; |
| | 1 | 229 | | ficto.FechaModificacion = DateTime.UtcNow; |
| | 1 | 230 | | ficto.UsuarioModificacionId = UsuarioValido(usuarioId); |
| | 1 | 231 | | await _repositorio.ActualizarAsync(ficto); |
| | | 232 | | |
| | 1 | 233 | | await _auditoria.LogAuditoriaAsync( |
| | 1 | 234 | | usuarioId, |
| | 1 | 235 | | AccionEnum.ReactivarFicto, |
| | 1 | 236 | | ContextoEnum.Descuentos, |
| | 1 | 237 | | host, |
| | 1 | 238 | | nameof(FictoPersona), |
| | 1 | 239 | | ficto.Id, |
| | 1 | 240 | | new |
| | 1 | 241 | | { |
| | 1 | 242 | | ficto.PersonaId, |
| | 1 | 243 | | ficto.CatalogoItemId, |
| | 1 | 244 | | ficto.Importe |
| | 1 | 245 | | }); |
| | | 246 | | |
| | 1 | 247 | | return MapearDto(ficto); |
| | 1 | 248 | | } |
| | | 249 | | |
| | | 250 | | public async Task<ResultadoValidacionImportacionFictosDto> ValidarImportacionAsync( |
| | | 251 | | ImportarFictosRequest request) |
| | | 252 | | { |
| | 2 | 253 | | var validacion = await ValidarLoteAsync(request); |
| | 2 | 254 | | return validacion.Resultado; |
| | 2 | 255 | | } |
| | | 256 | | |
| | | 257 | | public async Task<ResultadoImportacionFictosDto> AplicarImportacionAsync( |
| | | 258 | | ImportarFictosRequest request, |
| | | 259 | | long usuarioId, |
| | | 260 | | string? host) |
| | | 261 | | { |
| | 2 | 262 | | IDbContextTransaction? transaccion = null; |
| | 2 | 263 | | if (_contexto.Database.IsRelational()) |
| | 0 | 264 | | transaccion = await _contexto.Database.BeginTransactionAsync(); |
| | | 265 | | |
| | | 266 | | try |
| | | 267 | | { |
| | 2 | 268 | | var validacion = await ValidarLoteAsync(request); |
| | 2 | 269 | | var resultado = CopiarResultado(validacion.Resultado); |
| | 2 | 270 | | if (!validacion.Resultado.EsValido) |
| | | 271 | | { |
| | 1 | 272 | | if (transaccion is not null) |
| | 0 | 273 | | await transaccion.RollbackAsync(); |
| | 1 | 274 | | return resultado; |
| | | 275 | | } |
| | | 276 | | |
| | 1 | 277 | | var ahora = DateTime.UtcNow; |
| | 1 | 278 | | var usuarioValido = UsuarioValido(usuarioId); |
| | 10 | 279 | | foreach (var fila in validacion.Filas) |
| | | 280 | | { |
| | 4 | 281 | | if (fila.Existente is null) |
| | | 282 | | { |
| | 1 | 283 | | _contexto.FictosPersona.Add(new FictoPersona |
| | 1 | 284 | | { |
| | 1 | 285 | | PersonaId = fila.Persona.Id, |
| | 1 | 286 | | AcreedorId = validacion.AcreedorMdn!.Id, |
| | 1 | 287 | | CatalogoItemId = fila.Catalogo.Id, |
| | 1 | 288 | | Importe = fila.Importe, |
| | 1 | 289 | | Activo = true, |
| | 1 | 290 | | FechaCreacion = ahora, |
| | 1 | 291 | | UsuarioCreacionId = usuarioValido |
| | 1 | 292 | | }); |
| | 1 | 293 | | continue; |
| | | 294 | | } |
| | | 295 | | |
| | 3 | 296 | | if (fila.SinCambios) |
| | | 297 | | continue; |
| | | 298 | | |
| | 2 | 299 | | fila.Existente.Importe = fila.Importe; |
| | 2 | 300 | | fila.Existente.AcreedorId = validacion.AcreedorMdn!.Id; |
| | 2 | 301 | | fila.Existente.Activo = true; |
| | 2 | 302 | | fila.Existente.FechaBaja = null; |
| | 2 | 303 | | fila.Existente.UsuarioBajaId = null; |
| | 2 | 304 | | fila.Existente.FechaModificacion = ahora; |
| | 2 | 305 | | fila.Existente.UsuarioModificacionId = usuarioValido; |
| | | 306 | | } |
| | | 307 | | |
| | 1 | 308 | | await _contexto.SaveChangesAsync(); |
| | 1 | 309 | | await _auditoria.LogAuditoriaAsync( |
| | 1 | 310 | | usuarioId, |
| | 1 | 311 | | AccionEnum.ImportarFictos, |
| | 1 | 312 | | ContextoEnum.Descuentos, |
| | 1 | 313 | | host, |
| | 1 | 314 | | nameof(FictoPersona), |
| | 1 | 315 | | null, |
| | 1 | 316 | | new |
| | 1 | 317 | | { |
| | 1 | 318 | | request.NombreArchivo, |
| | 1 | 319 | | request.HashArchivo, |
| | 1 | 320 | | resultado.TotalFilas, |
| | 1 | 321 | | resultado.Nuevos, |
| | 1 | 322 | | resultado.Actualizados, |
| | 1 | 323 | | resultado.Reactivados, |
| | 1 | 324 | | resultado.SinCambios |
| | 1 | 325 | | }); |
| | | 326 | | |
| | 1 | 327 | | if (transaccion is not null) |
| | 0 | 328 | | await transaccion.CommitAsync(); |
| | 1 | 329 | | resultado.Aplicado = true; |
| | 1 | 330 | | return resultado; |
| | | 331 | | } |
| | 0 | 332 | | catch |
| | | 333 | | { |
| | 0 | 334 | | if (transaccion is not null) |
| | 0 | 335 | | await transaccion.RollbackAsync(); |
| | 0 | 336 | | throw; |
| | | 337 | | } |
| | | 338 | | finally |
| | | 339 | | { |
| | 2 | 340 | | if (transaccion is not null) |
| | 0 | 341 | | await transaccion.DisposeAsync(); |
| | | 342 | | } |
| | 2 | 343 | | } |
| | | 344 | | |
| | | 345 | | private async Task<ValidacionLote> ValidarLoteAsync(ImportarFictosRequest request) |
| | | 346 | | { |
| | 4 | 347 | | var resultado = new ResultadoValidacionImportacionFictosDto |
| | 4 | 348 | | { |
| | 4 | 349 | | TotalFilas = request.Filas?.Count ?? 0 |
| | 4 | 350 | | }; |
| | | 351 | | |
| | 4 | 352 | | if (request.Filas is null || request.Filas.Count == 0) |
| | | 353 | | { |
| | 0 | 354 | | AgregarError(resultado, string.Empty, 0, string.Empty, "El archivo no contiene filas para importar."); |
| | 0 | 355 | | return new ValidacionLote(resultado, [], null); |
| | | 356 | | } |
| | | 357 | | |
| | 4 | 358 | | if (request.Filas.Count > 20000) |
| | | 359 | | { |
| | 0 | 360 | | AgregarError(resultado, string.Empty, 0, string.Empty, "La importación no puede superar 10.000 filas por hoj |
| | 0 | 361 | | return new ValidacionLote(resultado, [], null); |
| | | 362 | | } |
| | | 363 | | |
| | 4 | 364 | | var acreedor = await _contexto.Acreedores |
| | 4 | 365 | | .FirstOrDefaultAsync(a => a.Codigo == CodigoAcreedorMdn && a.Activo); |
| | 4 | 366 | | if (acreedor is null) |
| | | 367 | | { |
| | 0 | 368 | | AgregarError(resultado, string.Empty, 0, string.Empty, "El acreedor MDN no existe o no está activo."); |
| | 0 | 369 | | return new ValidacionLote(resultado, [], null); |
| | | 370 | | } |
| | | 371 | | |
| | 4 | 372 | | var catalogos = new Dictionary<string, CatalogoItem>(StringComparer.OrdinalIgnoreCase); |
| | 25 | 373 | | foreach (var tipo in request.Filas.Select(f => NormalizarTipo(f.Tipo)).Distinct(StringComparer.OrdinalIgnoreCase |
| | | 374 | | { |
| | 4 | 375 | | if (!TiposFicto.Importables.Contains(tipo)) |
| | | 376 | | { |
| | 5 | 377 | | foreach (var fila in request.Filas.Where(f => NormalizarTipo(f.Tipo) == tipo)) |
| | 1 | 378 | | AgregarError(resultado, tipo, fila.NumeroFila, fila.Cedula, "Solo se pueden importar comedor y guard |
| | | 379 | | continue; |
| | | 380 | | } |
| | | 381 | | |
| | | 382 | | try |
| | | 383 | | { |
| | 3 | 384 | | catalogos[tipo] = await ResolverCatalogoAsync(tipo); |
| | 3 | 385 | | } |
| | 0 | 386 | | catch (Exception ex) when (ex is ArgumentException or InvalidOperationException) |
| | | 387 | | { |
| | 0 | 388 | | AgregarError(resultado, tipo, 0, string.Empty, ex.Message); |
| | 0 | 389 | | } |
| | 3 | 390 | | } |
| | | 391 | | |
| | 4 | 392 | | var filasNormalizadas = new List<FilaNormalizada>(); |
| | 4 | 393 | | var claves = new HashSet<string>(StringComparer.OrdinalIgnoreCase); |
| | 26 | 394 | | foreach (var fila in request.Filas) |
| | | 395 | | { |
| | 9 | 396 | | var tipo = NormalizarTipo(fila.Tipo); |
| | 9 | 397 | | var cedula = NormalizarCedulaSinExcepcion(fila.Cedula); |
| | 9 | 398 | | if (!TiposFicto.Importables.Contains(tipo) || !catalogos.ContainsKey(tipo)) |
| | | 399 | | continue; |
| | 8 | 400 | | if (!Regex.IsMatch(cedula, @"^\d{6,7}$")) |
| | | 401 | | { |
| | 0 | 402 | | AgregarError(resultado, tipo, fila.NumeroFila, cedula, "La cédula debe contener 6 o 7 dígitos (sin dígit |
| | 0 | 403 | | continue; |
| | | 404 | | } |
| | 8 | 405 | | cedula = CedulaValidator.CompletarCedula(cedula); |
| | 8 | 406 | | if (fila.Importe <= 0) |
| | | 407 | | { |
| | 0 | 408 | | AgregarError(resultado, tipo, fila.NumeroFila, cedula, "El importe debe ser mayor a cero."); |
| | 0 | 409 | | continue; |
| | | 410 | | } |
| | | 411 | | |
| | 8 | 412 | | var clave = $"{tipo}|{cedula}"; |
| | 8 | 413 | | if (!claves.Add(clave)) |
| | | 414 | | { |
| | 1 | 415 | | AgregarError(resultado, tipo, fila.NumeroFila, cedula, "La cédula está duplicada para el mismo tipo de f |
| | 1 | 416 | | continue; |
| | | 417 | | } |
| | | 418 | | |
| | 7 | 419 | | filasNormalizadas.Add(new FilaNormalizada( |
| | 7 | 420 | | tipo, |
| | 7 | 421 | | fila.NumeroFila, |
| | 7 | 422 | | cedula, |
| | 7 | 423 | | RedondearImporte(fila.Importe), |
| | 7 | 424 | | catalogos[tipo])); |
| | | 425 | | } |
| | | 426 | | |
| | 11 | 427 | | var cedulas = filasNormalizadas.Select(f => f.Cedula).Distinct().ToList(); |
| | 4 | 428 | | var relaciones = await _contexto.RelacionesLaborales |
| | 4 | 429 | | .AsNoTracking() |
| | 4 | 430 | | .Include(r => r.Persona) |
| | 4 | 431 | | .Where(r => r.Estado == EstadoRelacion.Activo && cedulas.Contains(r.Persona.Cedula)) |
| | 4 | 432 | | .ToListAsync(); |
| | 4 | 433 | | var personasPorCedula = relaciones |
| | 6 | 434 | | .GroupBy(r => r.Persona.Cedula) |
| | 16 | 435 | | .ToDictionary(g => g.Key, g => g.First().Persona); |
| | | 436 | | |
| | 17 | 437 | | foreach (var fila in filasNormalizadas.Where(f => !personasPorCedula.ContainsKey(f.Cedula))) |
| | 1 | 438 | | AgregarError(resultado, fila.Tipo, fila.NumeroFila, fila.Cedula, "No se encontró personal activo con esa céd |
| | | 439 | | |
| | 4 | 440 | | var filasConPersona = filasNormalizadas |
| | 7 | 441 | | .Where(f => personasPorCedula.ContainsKey(f.Cedula)) |
| | 4 | 442 | | .ToList(); |
| | 10 | 443 | | var personaIds = filasConPersona.Select(f => personasPorCedula[f.Cedula].Id).Distinct().ToArray(); |
| | 10 | 444 | | var catalogoIds = filasConPersona.Select(f => f.Catalogo.Id).Distinct().ToArray(); |
| | 4 | 445 | | var existentes = await _repositorio.ObtenerPorPersonasYCatalogosAsync(personaIds, catalogoIds); |
| | 7 | 446 | | var existentesPorClave = existentes.ToDictionary(f => (f.PersonaId, f.CatalogoItemId)); |
| | | 447 | | |
| | 4 | 448 | | var filasResueltas = new List<FilaResuelta>(); |
| | 20 | 449 | | foreach (var fila in filasConPersona) |
| | | 450 | | { |
| | 6 | 451 | | var persona = personasPorCedula[fila.Cedula]; |
| | 6 | 452 | | existentesPorClave.TryGetValue((persona.Id, fila.Catalogo.Id), out var existente); |
| | 6 | 453 | | var sinCambios = existente is not null && |
| | 6 | 454 | | existente.Activo && |
| | 6 | 455 | | existente.Importe == fila.Importe && |
| | 6 | 456 | | existente.AcreedorId == acreedor.Id; |
| | | 457 | | |
| | 6 | 458 | | if (existente is null) |
| | 3 | 459 | | resultado.Nuevos++; |
| | 3 | 460 | | else if (!existente.Activo) |
| | 1 | 461 | | resultado.Reactivados++; |
| | 2 | 462 | | else if (sinCambios) |
| | 1 | 463 | | resultado.SinCambios++; |
| | | 464 | | else |
| | 1 | 465 | | resultado.Actualizados++; |
| | | 466 | | |
| | 6 | 467 | | filasResueltas.Add(new FilaResuelta( |
| | 6 | 468 | | fila.Tipo, |
| | 6 | 469 | | fila.NumeroFila, |
| | 6 | 470 | | fila.Cedula, |
| | 6 | 471 | | fila.Importe, |
| | 6 | 472 | | fila.Catalogo, |
| | 6 | 473 | | persona, |
| | 6 | 474 | | existente, |
| | 6 | 475 | | sinCambios)); |
| | | 476 | | } |
| | | 477 | | |
| | 4 | 478 | | return new ValidacionLote(resultado, filasResueltas, acreedor); |
| | 4 | 479 | | } |
| | | 480 | | |
| | | 481 | | private async Task<CatalogoItem> ResolverCatalogoAsync(string tipo) |
| | | 482 | | { |
| | 13 | 483 | | tipo = NormalizarTipo(tipo); |
| | 13 | 484 | | if (!CodigoPorTipo.TryGetValue(tipo, out var codigo)) |
| | 0 | 485 | | throw new ArgumentException($"El tipo de ficto '{tipo}' no es válido."); |
| | | 486 | | |
| | 13 | 487 | | var catalogo = await _contexto.CatalogoItems |
| | 13 | 488 | | .AsNoTracking() |
| | 13 | 489 | | .FirstOrDefaultAsync(c => |
| | 13 | 490 | | c.Codigo == codigo && |
| | 13 | 491 | | c.Tipo == "descuento_personal" && |
| | 13 | 492 | | c.Vigente); |
| | 13 | 493 | | if (catalogo is null) |
| | 0 | 494 | | throw new InvalidOperationException($"El concepto de {tipo} no existe o no está vigente."); |
| | 13 | 495 | | if (catalogo.ItemParFictoId is null) |
| | 0 | 496 | | throw new InvalidOperationException($"El concepto de {tipo} no tiene configurado su par ficto."); |
| | | 497 | | |
| | 13 | 498 | | var codigoEspejoEsperado = CodigoEspejoPorTipo[tipo]; |
| | 13 | 499 | | var conceptoEspejo = await _contexto.CatalogoItems |
| | 13 | 500 | | .AsNoTracking() |
| | 13 | 501 | | .FirstOrDefaultAsync(c => c.Id == catalogo.ItemParFictoId.Value); |
| | 13 | 502 | | if (conceptoEspejo is null || |
| | 13 | 503 | | !conceptoEspejo.Vigente || |
| | 13 | 504 | | conceptoEspejo.Codigo != codigoEspejoEsperado || |
| | 13 | 505 | | !string.Equals(conceptoEspejo.Tipo, "ficto_descuento", StringComparison.OrdinalIgnoreCase) || |
| | 13 | 506 | | conceptoEspejo.ItemParFictoId != catalogo.Id) |
| | | 507 | | { |
| | 1 | 508 | | throw new InvalidOperationException( |
| | 1 | 509 | | $"El concepto de {tipo} no tiene configurado correctamente el concepto espejo {codigoEspejoEsperado}."); |
| | | 510 | | } |
| | | 511 | | |
| | 12 | 512 | | return catalogo; |
| | 12 | 513 | | } |
| | | 514 | | |
| | | 515 | | private async Task<Acreedor> ResolverMdnAsync() => |
| | 9 | 516 | | await _contexto.Acreedores |
| | 9 | 517 | | .AsNoTracking() |
| | 9 | 518 | | .FirstOrDefaultAsync(a => a.Codigo == CodigoAcreedorMdn && a.Activo) |
| | 9 | 519 | | ?? throw new InvalidOperationException("El acreedor MDN no existe o no está activo."); |
| | | 520 | | |
| | | 521 | | private static string NormalizarTipo(string? tipo) |
| | | 522 | | { |
| | 40 | 523 | | var texto = (tipo ?? string.Empty).Trim().ToLowerInvariant().Normalize(NormalizationForm.FormD); |
| | 40 | 524 | | var caracteres = texto.Where(c => |
| | 331 | 525 | | System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c) != |
| | 331 | 526 | | System.Globalization.UnicodeCategory.NonSpacingMark); |
| | 40 | 527 | | return new string(caracteres.ToArray()).Normalize(NormalizationForm.FormC); |
| | | 528 | | } |
| | | 529 | | |
| | | 530 | | private static string NormalizarCedula(string cedula) |
| | | 531 | | { |
| | 10 | 532 | | var normalizada = NormalizarCedulaSinExcepcion(cedula); |
| | 10 | 533 | | if (!Regex.IsMatch(normalizada, @"^\d{7,8}$")) |
| | 0 | 534 | | throw new ArgumentException("La cédula debe contener 7 u 8 dígitos."); |
| | 10 | 535 | | return normalizada; |
| | | 536 | | } |
| | | 537 | | |
| | | 538 | | private static string NormalizarCedulaSinExcepcion(string? cedula) => |
| | 19 | 539 | | CedulaValidator.Limpiar(cedula ?? string.Empty); |
| | | 540 | | |
| | | 541 | | private static decimal RedondearImporte(decimal importe) => |
| | 15 | 542 | | Math.Round(importe, 2, MidpointRounding.AwayFromZero); |
| | | 543 | | |
| | | 544 | | private static void ValidarImporte(decimal importe) |
| | | 545 | | { |
| | 12 | 546 | | if (importe <= 0) |
| | 0 | 547 | | throw new ArgumentException("El importe debe ser mayor a cero."); |
| | 12 | 548 | | } |
| | | 549 | | |
| | | 550 | | private static string? NormalizarObservaciones(string? observaciones) => |
| | 8 | 551 | | string.IsNullOrWhiteSpace(observaciones) ? null : observaciones.Trim(); |
| | | 552 | | |
| | 12 | 553 | | private static long? UsuarioValido(long usuarioId) => usuarioId > 0 ? usuarioId : null; |
| | | 554 | | |
| | 9 | 555 | | private static FictoPersonaDto MapearDto(FictoPersona ficto) => new() |
| | 9 | 556 | | { |
| | 9 | 557 | | Id = ficto.Id, |
| | 9 | 558 | | PersonaId = ficto.PersonaId, |
| | 9 | 559 | | Cedula = ficto.Persona.Cedula, |
| | 9 | 560 | | NombreCompleto = ficto.Persona.NombreCompleto, |
| | 9 | 561 | | Tipo = TipoPorCodigo.GetValueOrDefault(ficto.CatalogoItem.Codigo, ficto.CatalogoItem.Nombre), |
| | 9 | 562 | | Importe = ficto.Importe, |
| | 9 | 563 | | Activo = ficto.Activo, |
| | 9 | 564 | | AcreedorCodigo = ficto.Acreedor.Codigo, |
| | 9 | 565 | | AcreedorNombre = ficto.Acreedor.Nombre, |
| | 9 | 566 | | Observaciones = ficto.Observaciones, |
| | 9 | 567 | | FechaCreacion = ficto.FechaCreacion, |
| | 9 | 568 | | FechaModificacion = ficto.FechaModificacion, |
| | 9 | 569 | | FechaBaja = ficto.FechaBaja |
| | 9 | 570 | | }; |
| | | 571 | | |
| | | 572 | | private static void AgregarError( |
| | | 573 | | ResultadoValidacionImportacionFictosDto resultado, |
| | | 574 | | string tipo, |
| | | 575 | | int numeroFila, |
| | | 576 | | string cedula, |
| | | 577 | | string mensaje) => |
| | 3 | 578 | | resultado.Errores.Add(new ErrorImportacionFictoDto |
| | 3 | 579 | | { |
| | 3 | 580 | | Tipo = tipo, |
| | 3 | 581 | | NumeroFila = numeroFila, |
| | 3 | 582 | | Cedula = cedula, |
| | 3 | 583 | | Mensaje = mensaje |
| | 3 | 584 | | }); |
| | | 585 | | |
| | | 586 | | private static ResultadoImportacionFictosDto CopiarResultado( |
| | 2 | 587 | | ResultadoValidacionImportacionFictosDto origen) => new() |
| | 2 | 588 | | { |
| | 2 | 589 | | TotalFilas = origen.TotalFilas, |
| | 2 | 590 | | Nuevos = origen.Nuevos, |
| | 2 | 591 | | Actualizados = origen.Actualizados, |
| | 2 | 592 | | Reactivados = origen.Reactivados, |
| | 2 | 593 | | SinCambios = origen.SinCambios, |
| | 2 | 594 | | Errores = origen.Errores.ToList(), |
| | 2 | 595 | | Aplicado = false |
| | 2 | 596 | | }; |
| | | 597 | | |
| | 7 | 598 | | private sealed record FilaNormalizada( |
| | 7 | 599 | | string Tipo, |
| | 7 | 600 | | int NumeroFila, |
| | 40 | 601 | | string Cedula, |
| | 8 | 602 | | decimal Importe, |
| | 25 | 603 | | CatalogoItem Catalogo); |
| | | 604 | | |
| | 6 | 605 | | private sealed record FilaResuelta( |
| | 0 | 606 | | string Tipo, |
| | 0 | 607 | | int NumeroFila, |
| | 0 | 608 | | string Cedula, |
| | 3 | 609 | | decimal Importe, |
| | 1 | 610 | | CatalogoItem Catalogo, |
| | 1 | 611 | | Persona Persona, |
| | 18 | 612 | | FictoPersona? Existente, |
| | 9 | 613 | | bool SinCambios); |
| | | 614 | | |
| | 4 | 615 | | private sealed record ValidacionLote( |
| | 6 | 616 | | ResultadoValidacionImportacionFictosDto Resultado, |
| | 1 | 617 | | IReadOnlyList<FilaResuelta> Filas, |
| | 7 | 618 | | Acreedor? AcreedorMdn); |
| | | 619 | | } |