diff --git a/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs b/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs index 38158c2..8202918 100644 --- a/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs +++ b/Backend/GestorFacturas.API/Services/ProcesadorFacturasService.cs @@ -37,6 +37,12 @@ public class ProcesadorFacturasService : IProcesadorFacturasService public async Task EjecutarProcesoAsync(DateTime fechaDesde) { + // Contadores inicializados AL PRINCIPIO + int copiadas = 0; + int omitidas = 0; + int errores = 0; + // No declarar 'int procesadas = 0' porque es redundante con copiadas/omitidas + var config = await _context.Configuraciones.FirstOrDefaultAsync(c => c.Id == 1); if (config == null) { @@ -44,7 +50,6 @@ public class ProcesadorFacturasService : IProcesadorFacturasService return; } - // Actualizamos la fecha de ejecución config.UltimaEjecucion = DateTime.Now; await _context.SaveChangesAsync(); @@ -64,20 +69,27 @@ public class ProcesadorFacturasService : IProcesadorFacturasService await RegistrarEventoAsync($"Se encontraron {facturas.Count} facturas para procesar", TipoEvento.Info); - int procesadas = 0; - int errores = 0; List pendientes = new(); List detallesErroresParaMail = new(); - - // Lista para rastrear las entidades de Evento y actualizar su flag 'Enviado' luego List eventosDeErrorParaActualizar = new(); // --- 1. Primer intento --- foreach (var factura in facturas) { - bool exito = await ProcesarFacturaAsync(factura, config); - if (exito) procesadas++; - else pendientes.Add(factura); + var resultado = await ProcesarFacturaAsync(factura, config); + + switch (resultado) + { + case ResultadoProceso.Copiado: + copiadas++; + break; + case ResultadoProceso.Omitido: + omitidas++; + break; + case ResultadoProceso.Error: + pendientes.Add(factura); + break; + } } // --- 2. Sistema de Reintentos --- @@ -93,11 +105,14 @@ public class ProcesadorFacturasService : IProcesadorFacturasService foreach (var factura in pendientes) { - bool exito = await ProcesarFacturaAsync(factura, config); - if (exito) + var resultado = await ProcesarFacturaAsync(factura, config); + + if (resultado != ResultadoProceso.Error) { - procesadas++; - _logger.LogInformation("Archivo encontrado en reintento {intento}: {archivo}", intento, factura.NombreArchivoOrigen); + if (resultado == ResultadoProceso.Copiado) copiadas++; + if (resultado == ResultadoProceso.Omitido) omitidas++; + + _logger.LogInformation("Archivo recuperado en reintento {intento}: {archivo}", intento, factura.NombreArchivoOrigen); } else { @@ -110,7 +125,7 @@ public class ProcesadorFacturasService : IProcesadorFacturasService // --- 3. Registro de Errores Finales --- if (pendientes.Count > 0) { - errores = pendientes.Count; + errores = pendientes.Count; // Aquí usamos la variable ya declarada arriba foreach (var factura in pendientes) { string msgError = $"El archivo NO EXISTE después de {MAX_REINTENTOS} intentos: {factura.NombreArchivoOrigen}"; @@ -124,8 +139,6 @@ public class ProcesadorFacturasService : IProcesadorFacturasService }; _context.Eventos.Add(eventoError); - - // Guardamos referencias eventosDeErrorParaActualizar.Add(eventoError); detallesErroresParaMail.Add(factura.NombreArchivoOrigen); } @@ -151,40 +164,31 @@ public class ProcesadorFacturasService : IProcesadorFacturasService catch { } // --- 6. Evento Final --- - var mensajeFinal = $"Proceso finalizado. Procesadas: {procesadas}, Errores: {errores}"; + var mensajeFinal = $"Proceso finalizado. Nuevas: {copiadas}, Verificadas: {omitidas}, Errores: {errores}"; await RegistrarEventoAsync(mensajeFinal, errores > 0 ? TipoEvento.Warning : TipoEvento.Info); - // --- 7. Envío de Mail Inteligente (Solo 1 vez por archivo) --- + // --- 7. Envío de Mail Inteligente --- if (errores > 0 && config.AvisoMail && !string.IsNullOrEmpty(config.SMTPDestinatario)) { - // 1. Buscamos en la historia de la DB si estos archivos ya fueron reportados previamente. - // Buscamos en TODOS los logs disponibles (que suelen ser los últimos 30 días según la limpieza). - // Filtramos por Tipo Error y Enviado=true. var historialErroresEnviados = await _context.Eventos .Where(e => e.Tipo == TipoEvento.Error.ToString() && e.Enviado == true) .Select(e => e.Mensaje) .ToListAsync(); - // 2. Filtramos la lista actual: - // Solo queremos los archivos que NO aparezcan en ningún mensaje del historial. var archivosNuevosParaNotificar = detallesErroresParaMail.Where(archivoFallido => { - // El mensaje en BD es: "El archivo NO EXISTE...: nombre_archivo.pdf" - // Chequeamos si el nombre del archivo está contenido en algún mensaje viejo. bool yaFueNotificado = historialErroresEnviados.Any(msgHistorico => msgHistorico.Contains(archivoFallido)); return !yaFueNotificado; }).ToList(); - // 3. Decidir si enviar mail bool mailEnviado = false; if (archivosNuevosParaNotificar.Count > 0) { - // Si hay archivos NUEVOS, enviamos mail SOLO con esos. - // Nota: Pasamos 'errores' (total técnico) y 'archivosNuevosParaNotificar' (detalle visual) + // Pasamos 'copiadas' en lugar de 'procesadas' para el mail, o la suma de ambas si prefieres mailEnviado = await EnviarNotificacionErroresAsync( config.SMTPDestinatario, - procesadas, + copiadas + omitidas, errores, archivosNuevosParaNotificar ); @@ -196,14 +200,10 @@ public class ProcesadorFacturasService : IProcesadorFacturasService } else { - _logger.LogInformation("Se omitió el envío de correo: Los {count} errores ya fueron notificados anteriormente.", errores); - // Simulamos que se "envió" (se gestionó) para marcar los flags en BD + _logger.LogInformation("Se omitió el envío de correo por errores repetidos."); mailEnviado = true; } - // 4. Actualizar flag en BD (CRÍTICO) - // Si gestionamos la notificación correctamente (ya sea enviándola o detectando que ya estaba enviada), - // marcamos los eventos actuales como Enviado=true para que pasen al historial y no se vuelvan a procesar. if (mailEnviado && eventosDeErrorParaActualizar.Count > 0) { foreach (var evento in eventosDeErrorParaActualizar) @@ -296,7 +296,7 @@ public class ProcesadorFacturasService : IProcesadorFacturasService return facturas; } - private async Task ProcesarFacturaAsync(FacturaParaProcesar factura, Configuracion config) + private async Task ProcesarFacturaAsync(FacturaParaProcesar factura, Configuracion config) { try { @@ -306,7 +306,8 @@ public class ProcesadorFacturasService : IProcesadorFacturasService string rutaOrigen = Path.Combine(rutaBaseOrigen, factura.NombreArchivoOrigen); string carpetaDestinoFinal = Path.Combine(rutaBaseDestino, factura.CarpetaDestino); - if (!File.Exists(rutaOrigen)) return false; + // Si no existe origen, es un error (para reintento) + if (!File.Exists(rutaOrigen)) return ResultadoProceso.Error; if (!Directory.Exists(carpetaDestinoFinal)) { @@ -319,16 +320,21 @@ public class ProcesadorFacturasService : IProcesadorFacturasService if (infoDestino.Exists) { - if (infoDestino.Length == infoOrigen.Length) return true; + // Si ya existe y es igual, lo OMITIMOS + if (infoDestino.Length == infoOrigen.Length) + { + return ResultadoProceso.Omitido; + } } + // Si llegamos acá, copiamos File.Copy(rutaOrigen, rutaDestinoCompleta, overwrite: true); - return true; + return ResultadoProceso.Copiado; } catch (Exception ex) { _logger.LogDebug(ex, "Fallo al copiar {archivo}", factura.NombreArchivoOrigen); - return false; + return ResultadoProceso.Error; } } @@ -481,3 +487,10 @@ public class FacturaParaProcesar public string NombreArchivoDestino { get; set; } = string.Empty; public string CarpetaDestino { get; set; } = string.Empty; } + +public enum ResultadoProceso +{ + Copiado, // Era nuevo y se copió + Omitido, // Ya existía y estaba bien + Error // No se encontró o falló +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 892f49a..5f5a0e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,6 @@ services: container_name: gestor_facturas_web restart: always ports: - - "80:80" + - "8080:80" depends_on: - backend \ No newline at end of file