Files
descarga_factura_transporte/tests/test_pipeline_flujo.py
T
GeorgeandClaude Sonnet 5.5 f0ad6aa78e se valida si la factura ya existe en SAP y se avisa cuando SAP está fuera de línea
- sapb1_manager.buscar_factura_existente busca la factura (proveedor, tipo,
  serie, número) en facturas de compra no anuladas y en drafts; si existe no
  se crea otro draft y se responde "ya está registrada" sin pisar el registro
  de una aceptación anterior.
- SapNoDisponible (subclase de ConnectionError) distingue una caída (timeout,
  sin conexión, HTTP 5xx) de un rechazo normal de SAP (HTTP 4xx). Una caída
  de Service Layer con HANA sin resultados ya no se lee como "guía no existe".
- Con SAP o HANA caídos se avisa una sola vez al proveedor con la plantilla
  "En cola" y el correo se reintenta en los ciclos siguientes (hasta 24), sin
  repetir avisos; al agotarse se avisa al equipo.
- La lógica por factura pasa a _procesar_factura; lo ya resuelto dentro de un
  correo no se repite al reintentarlo.
- seguimiento: columna intentos (con migración), registrar_intento y el
  estado de notificación en_espera.
- database_utilities.DB_NAME pasa a ser absoluto (junto al código).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 12:08:38 -05:00

586 lines
24 KiB
Python

"""
Flujo completo de ``pipeline._procesar_un_correo`` con SAP, HANA, Outlook y Graph
simulados. Usa el correo de prueba 001 (factura F002-2450, una línea de 624.38
con la guía T001-128912).
Cada factura genera dos correos: el **técnico**, solo para el equipo (asunto
"Draft SAP B1 - …"), y la **respuesta al proveedor** con la plantilla de aceptado o
rechazado (asunto "Aceptado · …" / "Rechazado · …").
"""
import glob
import logging
import os
import shutil
import sqlite3
import pytest
import database_utilities
import hana_manager
import outlook_utilities
import pipeline
import sapb1_manager
import seguimiento
from conftest import FIXTURES
CASO = "001_correo"
TOTAL_LINEA = 624.38
SENDER = "proveedor@empresa.com"
EQUIPO = ["equipo1@aberio.com", "equipo2@aberio.com"]
class Entorno:
"""Registra lo que el pipeline le pide a los servicios externos y permite alterar sus respuestas."""
def __init__(self):
self.line_total_sap = TOTAL_LINEA
self.guia_existe = True # False: SAP no devuelve ningún documento para la guía
self.sap_lanza = None # excepción que lanza get_delivery_notes
self.error_al_crear = None
self.hana_disponible = True
self.existente = None # dict: la factura ya existe en SAP
self.sap_caido = False # SAP no responde (en la búsqueda de la factura)
self.sap_caido_en_validacion = False
self.archivos = None # None: usa los del caso 001; dict {nombre: bytes}: esos archivos
self.drafts = []
self.correos = []
self.descargas = 0
@property
def tecnicos(self):
return [c for c in self.correos if "Draft SAP B1" in c["asunto"]]
@property
def respuestas(self):
return [c for c in self.correos if " · " in c["asunto"]
and any(p in c["asunto"] for p in ("Aceptado", "Rechazado", "En cola"))]
@pytest.fixture
def entorno(tmp_path, monkeypatch):
e = Entorno()
monkeypatch.setenv("RUTA_RED_FACTURACION", "")
monkeypatch.setenv("NOTIFICACION_EQUIPO", ",".join(EQUIPO))
monkeypatch.setattr(database_utilities, "get_existing_ids", lambda ids: [])
def descargar(addr, id_, token, carpeta):
e.descargas += 1
os.makedirs(carpeta, exist_ok=True) # como el download_attachments real
nombres = []
if e.archivos is None:
for ruta in glob.glob(os.path.join(FIXTURES, CASO, "*")):
if ruta.lower().endswith((".pdf", ".xml")):
shutil.copy(ruta, carpeta)
nombres.append(os.path.basename(ruta))
else:
for nombre, contenido in e.archivos.items():
with open(os.path.join(carpeta, nombre), "wb") as f:
f.write(contenido)
nombres.append(nombre)
return nombres
def enviar(token, desde, para, asunto, html, adjuntos=None, cc_recipients=None, bcc_recipients=None):
e.correos.append({"para": para, "asunto": asunto, "html": html, "cc": cc_recipients,
"bcc": bcc_recipients, "adjuntos": adjuntos})
return True
def crear_draft(draft):
if e.error_al_crear:
raise e.error_al_crear
e.drafts.append(draft)
return {"DocEntry": 777}
def delivery_notes(**k):
if e.sap_lanza:
raise e.sap_lanza
if not e.guia_existe:
return []
return [{"Series": "1", "DocNum": "9", "U_IDC_DESTINO": "Z1", "DocumentLines": [
{"ItemCode": "FL10007", "LineTotal": e.line_total_sap, "PriceAfterVAT": 1.3396,
"Quantity": 466, "WarehouseCode": "A-CENTRA", "Weight1": 1}]}]
class ConexionHana:
def close(self):
pass
monkeypatch.setattr(outlook_utilities, "download_attachments", descargar)
monkeypatch.setattr(outlook_utilities, "send_email", enviar)
def buscar_existente(ruc, tipo, serie, numero):
if e.sap_caido:
raise sapb1_manager.SapNoDisponible("No se pudo conectar con SAP B1")
return e.existente
def zonas():
if e.sap_caido_en_validacion:
raise sapb1_manager.SapNoDisponible("Timeout en Drafts")
return [{"Code": "Z1", "Name": "ZONA1"}]
monkeypatch.setattr(sapb1_manager, "buscar_factura_existente", buscar_existente)
monkeypatch.setattr(sapb1_manager, "get_zonas", zonas)
monkeypatch.setattr(sapb1_manager, "get_delivery_notes", delivery_notes)
monkeypatch.setattr(sapb1_manager, "obtener_condicion_pago", lambda c: {"NumberOfAdditionalDays": 30})
monkeypatch.setattr(sapb1_manager, "create_draft", crear_draft)
monkeypatch.setattr(hana_manager, "obtener_valor_referencial", lambda s, n: 0.0)
monkeypatch.setattr(hana_manager, "get_connection", lambda: ConexionHana() if e.hana_disponible else None)
e.descarga = tmp_path / "descarga"
e.salida = tmp_path / "salida"
return e
def _procesar(e, sender=SENDER, subject="Factura", id_="abc", es_automatico=False, carpeta=None):
email = {"id": id_, "fecha": "2026-10-14 10:35:00", "subject": subject, "sender": sender,
"has_attach": True, "es_automatico": es_automatico}
pipeline._procesar_un_correo(email, "token", "buzon@aberio.com", str(carpeta or e.descarga), str(e.salida))
return email
def _registros():
conn = sqlite3.connect(database_utilities.DB_NAME)
conn.row_factory = sqlite3.Row
try:
return [dict(f) for f in conn.execute("SELECT * FROM comprobantes ORDER BY id")]
finally:
conn.close()
# ============================================================================
# Aceptado
# ============================================================================
def test_aceptado_crea_draft_y_responde_con_la_plantilla(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno)
assert len(entorno.drafts) == 1
draft = entorno.drafts[0]
assert draft["CardCode"] == "PL10060028686" and draft["NumAtCard"] == "01F002-2450"
(respuesta,) = entorno.respuestas
assert respuesta["para"] == [SENDER] # al que envió el correo
assert respuesta["bcc"] == EQUIPO # el equipo en copia oculta
assert respuesta["cc"] is None and respuesta["adjuntos"] is None
assert respuesta["asunto"].startswith("Aceptado · Factura F002-2450 · ABR-")
assert "Registramos su factura" in respuesta["html"]
assert "09T001-128912" not in respuesta["html"] and "T001-128912" in respuesta["html"]
assert "S/ 624.38" in respuesta["html"]
assert "Pago estimado" in respuesta["html"] # hay condición de pago de 30 días
def test_el_correo_tecnico_va_solo_al_equipo_nunca_al_remitente(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno)
(tecnico,) = entorno.tecnicos
assert tecnico["para"] == EQUIPO
assert SENDER not in tecnico["para"] and not tecnico["cc"] and not tecnico["bcc"]
assert any(a.endswith(".xml") for a in tecnico["adjuntos"]) # lleva los adjuntos internos
assert f"<b>Remitente:</b> {SENDER}" in tecnico["html"] and "ABR-" in tecnico["html"]
assert SENDER not in [d for c in entorno.respuestas for d in c["para"] if c["asunto"].startswith("[")]
def test_modo_prueba_es_el_predeterminado_y_no_escribe_al_remitente(entorno):
_procesar(entorno) # NOTIFICACION_MODO sin definir
(respuesta,) = entorno.respuestas
assert respuesta["para"] == EQUIPO
assert respuesta["bcc"] is None
assert respuesta["asunto"].startswith(f"[PRUEBA → {SENDER}] Aceptado · Factura F002-2450")
assert all(SENDER not in c["para"] for c in entorno.correos) # a nadie fuera del equipo
# ============================================================================
# Rechazado
# ============================================================================
def test_precios_distintos_rechaza_y_muestra_ambos_importes(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.line_total_sap = TOTAL_LINEA + 50
_procesar(entorno)
assert entorno.drafts == []
(respuesta,) = entorno.respuestas
assert respuesta["para"] == [SENDER]
assert respuesta["asunto"].startswith("Rechazado · Factura F002-2450")
html = respuesta["html"]
assert "No pudimos registrar su factura" in html and "1 observación" in html
assert "El importe no coincide con nuestro registro" in html
assert "S/ 624.38" in html and "S/ 674.38" in html # importe de la factura y esperado
assert "No coincide" in html
def test_guia_inexistente_rechaza_con_guia_no_encontrada(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.guia_existe = False
_procesar(entorno)
(respuesta,) = entorno.respuestas
assert "No encontramos la guía T001-128912" in respuesta["html"]
assert entorno.drafts == []
def test_guia_inexistente_con_hana_caido_es_servicio_fuera_de_linea_y_no_rechazo(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.guia_existe = False
entorno.hana_disponible = False
_procesar(entorno)
assert entorno.drafts == []
(aviso,) = entorno.respuestas # no se rechaza: se avisa que está en cola
assert aviso["asunto"].startswith("En cola · Factura F002-2450") and "No encontramos la guía" not in aviso["html"]
(tecnico,) = entorno.tecnicos
assert "HANA" in tecnico["html"] and tecnico["asunto"].startswith("SAP fuera de línea")
(registro,) = _registros()
assert registro["estado"] == "error" and registro["notificacion_estado"] == "en_espera"
def test_error_consultando_sap_es_error_interno_y_no_rechazo(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.sap_lanza = RuntimeError("sin conexión")
_procesar(entorno)
assert entorno.respuestas == [] and entorno.drafts == []
assert len(entorno.tecnicos) == 1
assert _registros()[0]["estado"] == "error"
def test_error_al_crear_draft_avisa_solo_al_equipo(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.error_al_crear = RuntimeError("Service Layer caído")
_procesar(entorno)
assert entorno.respuestas == []
(tecnico,) = entorno.tecnicos
assert "Error al crear Draft" in tecnico["html"] and "Service Layer caído" in tecnico["html"]
(registro,) = _registros()
assert registro["estado"] == "error" and registro["notificacion_estado"] == "omitida"
# ============================================================================
# Correos sin comprobante válido
# ============================================================================
def test_correo_con_adjuntos_que_no_son_pdf_ni_xml_se_rechaza(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.archivos = {"foto.png": b"x", "nota.docx": b"y"}
_procesar(entorno)
(respuesta,) = entorno.respuestas
assert respuesta["para"] == [SENDER]
assert respuesta["asunto"].startswith("Rechazado · Comprobante no recibido · ABR-")
html = respuesta["html"]
assert "No recibimos el XML ni el PDF de su comprobante" in html
assert "Guías revisadas" not in html # sin tabla de guías vacía
assert "RUC —" not in html and "Factura electrónica" not in html # sin datos de comprobante inventados
assert html.count("RUC") == 1 # solo el del pie (RUC de A. Berio)
assert "{{" not in html
assert entorno.tecnicos == [] and entorno.drafts == []
(registro,) = _registros()
assert registro["serie"] is None and registro["estado"] == "rechazado"
assert registro["notificacion_estado"] == "enviada" and registro["notificacion_destinatario"] == SENDER
def test_solo_pdf_sin_xml_pide_el_xml(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
pdf = glob.glob(os.path.join(FIXTURES, "002_correo", "*.pdf"))[0]
entorno.archivos = {os.path.basename(pdf): open(pdf, "rb").read()}
_procesar(entorno)
(respuesta,) = entorno.respuestas
assert "Falta el archivo XML de su comprobante" in respuesta["html"]
def test_correo_sin_adjuntos_no_se_responde(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.archivos = {}
_procesar(entorno)
assert entorno.correos == [] and _registros() == []
@pytest.mark.parametrize("sender, subject, automatico, motivo", [
("no-reply@empresa.com", "Factura", False, "automática"),
("buzon@aberio.com", "Factura", False, "propio buzón"),
("persona@local", "Factura", False, "no válida"),
(SENDER, "Fuera de oficina", True, "automático"),
(SENDER, "Re: Aceptado · Factura F002-1 · ABR-261019-00042", False, "respuesta a una notificación"),
(SENDER, "RE: gracias", False, "respuesta a una notificación"),
])
def test_correos_sin_comprobante_a_los_que_no_se_responde(entorno, monkeypatch, sender, subject, automatico, motivo):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.archivos = {"foto.png": b"x"}
_procesar(entorno, sender=sender, subject=subject, es_automatico=automatico)
assert entorno.correos == []
(registro,) = _registros()
assert registro["notificacion_estado"] == "omitida" and motivo in registro["notificacion_error"]
def test_limite_de_respuestas_por_remitente(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.archivos = {"foto.png": b"x"}
for n in range(4):
_procesar(entorno, id_=f"correo-{n}", carpeta=entorno.descarga.parent / f"d{n}")
assert [r["notificacion_estado"] for r in _registros()] == ["enviada", "enviada", "enviada", "omitida"]
assert len(entorno.respuestas) == 3
# ============================================================================
# Remitente y registro
# ============================================================================
def test_remitente_desconocido_crea_el_draft_pero_no_responde(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno, sender="Desconocido")
assert len(entorno.drafts) == 1
assert entorno.respuestas == [] and len(entorno.tecnicos) == 1
assert _registros()[0]["notificacion_estado"] == "omitida"
def test_correo_ya_procesado_se_salta(entorno, monkeypatch):
monkeypatch.setattr(database_utilities, "get_existing_ids", lambda ids: ["abc"])
_procesar(entorno)
assert entorno.descargas == 0 and entorno.drafts == [] and entorno.correos == []
def test_guarda_resultados_y_draft_en_disco(entorno):
_procesar(entorno)
nombres = os.listdir(entorno.salida)
assert any(n.startswith("xml_resultado_F002-2450") for n in nombres)
assert any(n.startswith("draft_sapb1_F002-2450") for n in nombres)
def test_aceptado_deja_el_resultado_y_la_notificacion_registrados(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno)
(registro,) = _registros()
assert (registro["estado"], registro["doc_entry"], registro["tipo"]) == ("aceptado", 777, "01")
assert (registro["serie"], registro["numero"], registro["ruc_emisor"]) == ("F002", "2450", "10060028686")
assert registro["notificacion_estado"] == "enviada" and registro["notificacion_destinatario"] == SENDER
def test_modo_prueba_registra_a_quien_se_envio_de_verdad(entorno):
_procesar(entorno)
assert _registros()[0]["notificacion_destinatario"] == ", ".join(EQUIPO)
def test_rechazo_guarda_las_observaciones_completas(entorno):
entorno.line_total_sap = TOTAL_LINEA + 50
_procesar(entorno)
(registro,) = _registros()
(observacion,) = seguimiento.obtener(registro["codigo_seguimiento"])["observaciones"]
assert observacion["codigo"] == "precio_no_coincide"
assert {"etiqueta": "Importe esperado", "valor": "S/ 674.38"} in observacion["filas"]
def test_comprobante_reenviado_conserva_el_codigo_y_se_advierte(entorno, caplog):
_procesar(entorno)
(primero,) = _registros()
with caplog.at_level(logging.WARNING, logger="pipeline"):
_procesar(entorno, id_="otro-correo", carpeta=entorno.descarga.parent / "descarga2")
(registro,) = _registros() # sigue habiendo una sola fila
assert registro["codigo_seguimiento"] == primero["codigo_seguimiento"]
assert registro["veces_recibido"] == 2
assert any("ya recibido antes" in r.getMessage() for r in caplog.records)
def test_si_falla_el_seguimiento_igual_se_crea_el_draft_pero_no_se_responde(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
def roto(*a, **k):
raise sqlite3.OperationalError("database is locked")
monkeypatch.setattr(seguimiento, "registrar_comprobante", roto)
monkeypatch.setattr(seguimiento, "actualizar_resultado", roto)
_procesar(entorno)
assert len(entorno.drafts) == 1
assert len(entorno.tecnicos) == 1 and entorno.respuestas == [] # sin código no se responde al proveedor
def test_un_fallo_inesperado_en_una_factura_no_tumba_el_ciclo(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
monkeypatch.setattr(pipeline.draft_sap, "build_sap_draft", lambda *a, **k: 1 / 0)
_procesar(entorno) # no debe lanzar excepción
assert entorno.drafts == [] and entorno.respuestas == []
assert "ZeroDivisionError" in entorno.tecnicos[0]["html"]
assert _registros()[0]["estado"] == "error"
# ============================================================================
# La factura ya existe en SAP
# ============================================================================
def _ids_de_correos_guardados():
conn = sqlite3.connect(database_utilities.DB_NAME)
try:
return [f[0] for f in conn.execute("SELECT id FROM emails")]
finally:
conn.close()
def test_factura_que_ya_existe_en_sap_no_crea_otro_draft_y_se_rechaza(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.existente = {"entidad": "PurchaseInvoices", "DocEntry": 55, "DocNum": 9}
_procesar(entorno)
assert entorno.drafts == []
(respuesta,) = entorno.respuestas
assert respuesta["para"] == [SENDER] and respuesta["asunto"].startswith("Rechazado · Factura F002-2450")
assert "Esta factura ya está registrada en nuestro sistema" in respuesta["html"]
(tecnico,) = entorno.tecnicos
assert "ya existe en SAP" in tecnico["html"] and "PurchaseInvoices 55" in tecnico["html"]
assert not any(n.startswith("draft_sapb1") for n in os.listdir(entorno.salida)) # ni se armó el draft
assert _registros()[0]["estado"] == "rechazado"
def test_el_reenvio_de_una_factura_aceptada_no_pisa_el_registro_original(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno) # se acepta: draft 777
entorno.existente = {"entidad": "Drafts", "DocEntry": 777, "DocNum": 3}
_procesar(entorno, id_="otro-correo", carpeta=entorno.descarga.parent / "descarga2")
(registro,) = _registros()
assert (registro["estado"], registro["doc_entry"], registro["veces_recibido"]) == ("aceptado", 777, 2)
assert len(entorno.drafts) == 1 # no se creó un segundo draft
assert "ya está registrada" in entorno.respuestas[-1]["html"]
def test_reprocesar_el_mismo_correo_ya_resuelto_no_repite_nada(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
_procesar(entorno)
enviados = len(entorno.correos)
_procesar(entorno) # mismo correo otra vez
assert len(entorno.drafts) == 1 and len(entorno.correos) == enviados
# ============================================================================
# SAP fuera de línea
# ============================================================================
def test_sap_fuera_de_linea_avisa_al_proveedor_y_deja_el_correo_en_cola(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.sap_caido = True
_procesar(entorno)
assert entorno.drafts == []
(aviso,) = entorno.respuestas
assert aviso["para"] == [SENDER] and aviso["asunto"].startswith("En cola · Factura F002-2450 · ABR-")
assert "Recibimos su factura" in aviso["html"] and "temporalmente fuera de línea" in aviso["html"]
assert "No necesita reenviarla" in aviso["html"] and "{{" not in aviso["html"]
(tecnico,) = entorno.tecnicos
assert tecnico["para"] == EQUIPO and tecnico["asunto"].startswith("SAP fuera de línea - Draft SAP B1")
(registro,) = _registros()
assert (registro["estado"], registro["notificacion_estado"], registro["intentos"]) == ("error", "en_espera", 1)
(obs,) = seguimiento.obtener(registro["codigo_seguimiento"])["observaciones"]
assert obs["codigo"] == "servicio_no_disponible" and obs["intentos"] == 1
assert "abc" not in _ids_de_correos_guardados() # olvidado: el próximo ciclo lo vuelve a tomar
def test_los_reintentos_no_repiten_el_aviso_y_al_volver_sap_se_responde(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.sap_caido = True
_procesar(entorno)
_procesar(entorno) # el ciclo siguiente: SAP sigue caído
assert len(entorno.respuestas) == 1 and len(entorno.tecnicos) == 1 # un solo aviso, no uno por ciclo
assert _registros()[0]["intentos"] == 2 and "abc" not in _ids_de_correos_guardados()
entorno.sap_caido = False
_procesar(entorno) # SAP volvió
asuntos = [c["asunto"] for c in entorno.respuestas]
assert asuntos[0].startswith("En cola") and asuntos[1].startswith("Aceptado") and len(asuntos) == 2
(registro,) = _registros()
assert (registro["estado"], registro["doc_entry"], registro["notificacion_estado"]) == ("aceptado", 777, "enviada")
assert len(entorno.drafts) == 1
assert "abc" in _ids_de_correos_guardados() # ya resuelto: no se vuelve a tomar
def test_se_agotan_los_reintentos_y_se_avisa_al_equipo(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
monkeypatch.setattr(pipeline, "MAX_REINTENTOS", 2)
entorno.sap_caido = True
_procesar(entorno)
_procesar(entorno)
assert [c["asunto"].split(" - ")[0] for c in entorno.tecnicos] == ["SAP fuera de línea", "Reintentos agotados"]
assert len(entorno.respuestas) == 1 # el proveedor solo recibió el aviso inicial
assert "abc" in _ids_de_correos_guardados() # deja de reintentarse
registro = _registros()[0]
assert registro["notificacion_estado"] == "omitida" and "reintentos" in registro["notificacion_error"]
@pytest.mark.parametrize("como_falla", ["validacion", "crear_draft"])
def test_la_caida_en_cualquier_paso_es_fuera_de_linea(entorno, monkeypatch, como_falla):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
if como_falla == "validacion":
entorno.sap_caido_en_validacion = True
else:
entorno.error_al_crear = sapb1_manager.SapNoDisponible("HTTP 503 - Service Unavailable")
_procesar(entorno)
assert entorno.drafts == []
(aviso,) = entorno.respuestas
assert aviso["asunto"].startswith("En cola · Factura F002-2450")
assert "abc" not in _ids_de_correos_guardados()
def test_un_rechazo_normal_de_sap_no_es_una_caida(entorno, monkeypatch):
"""HTTP 400 al crear el draft: es un problema del dato o de SAP, no se reintenta ni se le dice nada al proveedor."""
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.error_al_crear = ConnectionError("Error en SAP POST Drafts: HTTP 400 - ItemCode inexistente")
_procesar(entorno)
assert entorno.respuestas == []
assert "HTTP 400" in entorno.tecnicos[0]["html"]
assert _registros()[0]["estado"] == "error" and "abc" in _ids_de_correos_guardados()
def test_sin_seguimiento_una_caida_no_se_reintenta(entorno, monkeypatch):
monkeypatch.setenv("NOTIFICACION_MODO", "real")
entorno.sap_caido = True
def roto(*a, **k):
raise sqlite3.OperationalError("database is locked")
monkeypatch.setattr(seguimiento, "registrar_comprobante", roto)
_procesar(entorno)
assert entorno.respuestas == [] and len(entorno.tecnicos) == 1
assert "abc" in _ids_de_correos_guardados() # sin la cuenta de intentos no se arriesga un bucle