Files
descarga_factura_transporte/tests/test_pipeline_flujo.py
T
GeorgeandClaude Sonnet 5.5 297cfde0e1 fase 1: registro de comprobantes con código de seguimiento
Nueva tabla comprobantes (creada en init_db) y módulo seguimiento.py: cada
comprobante recibe un código ABR-AAMMDD-NNNNN que conserva si reaparece en
otro correo, más su resultado (aceptado/rechazado/error) y el estado de la
notificación. El pipeline lo registra por factura sin bloquear el flujo si el
seguimiento falla. Las pruebas usan una BD temporal para no tocar emails.db.

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

225 lines
7.8 KiB
Python

"""
Flujo completo de ``pipeline._procesar_un_correo`` con SAP, HANA, Outlook y la
BD simulados. Usa el correo de prueba 001 (factura F002-2450, una línea de
624.38 con la guía T001-128912).
"""
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
class Entorno:
"""Registra lo que el pipeline le pide a los servicios externos."""
def __init__(self):
self.line_total_sap = TOTAL_LINEA
self.error_al_crear = None
self.drafts = []
self.correos = []
self.descargas = 0
@pytest.fixture
def entorno(tmp_path, monkeypatch):
e = Entorno()
monkeypatch.setenv("RUTA_RED_FACTURACION", "")
monkeypatch.setattr(database_utilities, "get_existing_ids", lambda ids: [])
monkeypatch.setattr(database_utilities, "save_emails", lambda emails: None)
def descargar(addr, id_, token, carpeta):
e.descargas += 1
os.makedirs(carpeta, exist_ok=True) # como el download_attachments real
nombres = []
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))
return nombres
def enviar(token, desde, para, asunto, html, adjuntos=None, cc_recipients=None):
e.correos.append({"para": para, "asunto": asunto, "html": html, "cc": cc_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):
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}]}]
monkeypatch.setattr(outlook_utilities, "download_attachments", descargar)
monkeypatch.setattr(outlook_utilities, "send_email", enviar)
monkeypatch.setattr(sapb1_manager, "get_zonas", lambda: [{"Code": "Z1", "Name": "ZONA1"}])
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)
e.descarga = tmp_path / "descarga"
e.salida = tmp_path / "salida"
return e
def _procesar(e, sender="proveedor@empresa.com"):
email = {"id": "abc", "fecha": "2026-10-14 10:00:00", "subject": "Factura", "sender": sender}
pipeline._procesar_un_correo(email, "token", "buzon@aberio.com", str(e.descarga), str(e.salida))
return email
def test_precios_coinciden_crea_draft_y_notifica(entorno):
_procesar(entorno)
assert len(entorno.drafts) == 1
draft = entorno.drafts[0]
assert draft["CardCode"] == "PL10060028686"
assert draft["NumAtCard"] == "01F002-2450"
assert draft["DocumentLines"][0]["U_IDC_GRFLETE"] == "09T001-128912"
assert len(entorno.correos) == 1
correo = entorno.correos[0]
assert correo["para"] == ["proveedor@empresa.com"]
assert correo["asunto"] == "Draft SAP B1 - F002-2450"
assert "creado exitosamente" in correo["html"] and "777" in correo["html"]
assert correo["cc"] # siempre se copia al equipo
assert any(a.endswith(".xml") for a in correo["adjuntos"])
def test_precios_no_coinciden_no_crea_draft_pero_notifica(entorno):
entorno.line_total_sap = TOTAL_LINEA + 50
_procesar(entorno)
assert entorno.drafts == []
assert len(entorno.correos) == 1
assert "Draft NO creado" in entorno.correos[0]["html"]
def test_error_al_crear_draft_se_notifica(entorno):
entorno.error_al_crear = RuntimeError("Service Layer caído")
_procesar(entorno)
assert entorno.drafts == []
assert len(entorno.correos) == 1
assert "Error al crear Draft" in entorno.correos[0]["html"]
assert "Service Layer caído" in entorno.correos[0]["html"]
def test_remitente_desconocido_no_envia_correo(entorno):
_procesar(entorno, sender="Desconocido")
assert len(entorno.drafts) == 1 # el draft se crea igual
assert entorno.correos == []
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
assert 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)
# ============================================================================
# Seguimiento
# ============================================================================
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()
def test_draft_creado_deja_el_comprobante_aceptado(entorno):
_procesar(entorno)
(registro,) = _registros()
assert registro["estado"] == "aceptado"
assert registro["doc_entry"] == 777
assert registro["codigo_seguimiento"].startswith("ABR-")
assert (registro["serie"], registro["numero"], registro["tipo"]) == ("F002", "2450", "01")
assert registro["ruc_emisor"] == "10060028686"
def test_precios_que_no_coinciden_dejan_el_comprobante_rechazado(entorno):
entorno.line_total_sap = TOTAL_LINEA + 50
_procesar(entorno)
(registro,) = _registros()
assert registro["estado"] == "rechazado"
assert registro["doc_entry"] is None
assert seguimiento.obtener(registro["codigo_seguimiento"])["observaciones"] == [{"codigo": "precios_no_coinciden"}]
def test_error_al_crear_draft_deja_el_comprobante_en_error(entorno):
entorno.error_al_crear = RuntimeError("Service Layer caído")
_procesar(entorno)
(registro,) = _registros()
assert registro["estado"] == "error"
obs = seguimiento.obtener(registro["codigo_seguimiento"])["observaciones"]
assert obs == [{"codigo": "error_sap", "detalle": "Service Layer caído"}]
def test_comprobante_reenviado_conserva_el_codigo_y_se_advierte(entorno, caplog):
_procesar(entorno)
(primero,) = _registros()
entorno.descarga = entorno.descarga.parent / "descarga2" # otra descarga, otro correo
with caplog.at_level(logging.WARNING, logger="pipeline"):
pipeline._procesar_un_correo(
{"id": "otro-correo", "fecha": "2026-10-19 11:00:00", "subject": "Reenvío", "sender": "p@e.com"},
"token", "buzon@aberio.com", str(entorno.descarga), str(entorno.salida),
)
(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(entorno, monkeypatch):
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.correos) == 1