- 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>
586 lines
24 KiB
Python
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
|