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>
This commit is contained in:
1 parent
1d6d869111
commit
30e9b160f8
9 files changed
+563
-6
No files matched your search
@@ -2,6 +2,8 @@ import os
|
||||
|
||||
import pytest
|
||||
|
||||
import database_utilities
|
||||
|
||||
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
FIXTURES = os.path.join(RAIZ, "tests", "fixtures", "correos_prueba")
|
||||
GOLDEN = os.path.join(RAIZ, "tests", "golden")
|
||||
@@ -11,3 +13,13 @@ GOLDEN = os.path.join(RAIZ, "tests", "golden")
|
||||
def actualizar_golden() -> bool:
|
||||
"""True si se ejecutó con ``UPDATE_GOLDEN=1`` (regenera los resultados esperados)."""
|
||||
return os.environ.get("UPDATE_GOLDEN") == "1"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def base_de_datos_temporal(tmp_path_factory, monkeypatch):
|
||||
"""
|
||||
Toda prueba usa una BD SQLite propia: ``DB_NAME`` es relativo al directorio
|
||||
de trabajo, así que sin esto se escribiría en el ``emails.db`` real.
|
||||
"""
|
||||
monkeypatch.setattr(database_utilities, "DB_NAME", str(tmp_path_factory.mktemp("bd") / "emails_prueba.db"))
|
||||
database_utilities.init_db()
|
||||
@@ -4,8 +4,10 @@ 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
|
||||
|
||||
@@ -14,6 +16,7 @@ import hana_manager
|
||||
import outlook_utilities
|
||||
import pipeline
|
||||
import sapb1_manager
|
||||
import seguimiento
|
||||
from conftest import FIXTURES
|
||||
|
||||
CASO = "001_correo"
|
||||
@@ -144,3 +147,78 @@ def test_guarda_resultados_y_draft_en_disco(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
|
||||
@@ -0,0 +1,149 @@
|
||||
import re
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
import database_utilities
|
||||
import seguimiento
|
||||
|
||||
COMPROBANTE = ("20537929520", "01", "F002", "486")
|
||||
|
||||
|
||||
def test_registrar_crea_codigo_y_estado_inicial():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
|
||||
assert re.fullmatch(r"ABR-\d{6}-\d{5}", r["codigo_seguimiento"])
|
||||
assert r["estado"] == "pendiente"
|
||||
assert r["notificacion_estado"] == "pendiente"
|
||||
assert r["veces_recibido"] == 1
|
||||
assert r["reenvio"] is False
|
||||
assert (r["ruc_emisor"], r["tipo"], r["serie"], r["numero"]) == COMPROBANTE
|
||||
assert r["observaciones"] is None and r["doc_entry"] is None
|
||||
|
||||
|
||||
def test_reprocesar_el_mismo_correo_conserva_todo():
|
||||
primero = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
segundo = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
|
||||
assert segundo["codigo_seguimiento"] == primero["codigo_seguimiento"]
|
||||
assert segundo["veces_recibido"] == 1
|
||||
assert segundo["reenvio"] is False
|
||||
|
||||
|
||||
def test_mismo_comprobante_en_otro_correo_conserva_el_codigo():
|
||||
primero = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
segundo = seguimiento.registrar_comprobante("correo-2", *COMPROBANTE)
|
||||
|
||||
assert segundo["codigo_seguimiento"] == primero["codigo_seguimiento"]
|
||||
assert segundo["veces_recibido"] == 2
|
||||
assert segundo["email_id"] == "correo-2" # queda el correo más reciente
|
||||
assert segundo["reenvio"] is True
|
||||
|
||||
|
||||
def test_el_reenvio_conserva_el_resultado_anterior():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
seguimiento.actualizar_resultado(r["codigo_seguimiento"], "aceptado", doc_entry=2021)
|
||||
|
||||
reenviado = seguimiento.registrar_comprobante("correo-2", *COMPROBANTE)
|
||||
|
||||
assert reenviado["estado"] == "aceptado" and reenviado["doc_entry"] == 2021
|
||||
|
||||
|
||||
def test_comprobantes_distintos_tienen_codigos_distintos():
|
||||
a = seguimiento.registrar_comprobante("c1", "20537929520", "01", "F002", "486")
|
||||
b = seguimiento.registrar_comprobante("c1", "20537929520", "01", "F002", "487") # otro número
|
||||
c = seguimiento.registrar_comprobante("c1", "20111111111", "01", "F002", "486") # otro proveedor
|
||||
d = seguimiento.registrar_comprobante("c1", "20537929520", "07", "F002", "486") # otro tipo
|
||||
|
||||
codigos = {x["codigo_seguimiento"] for x in (a, b, c, d)}
|
||||
assert len(codigos) == 4
|
||||
|
||||
|
||||
def test_registrar_correo_es_idempotente_por_correo():
|
||||
a = seguimiento.registrar_correo("correo-1")
|
||||
de_nuevo = seguimiento.registrar_correo("correo-1")
|
||||
otro = seguimiento.registrar_correo("correo-2")
|
||||
|
||||
assert de_nuevo["codigo_seguimiento"] == a["codigo_seguimiento"]
|
||||
assert otro["codigo_seguimiento"] != a["codigo_seguimiento"]
|
||||
assert a["serie"] is None and a["ruc_emisor"] is None
|
||||
|
||||
|
||||
def test_correo_sin_comprobante_y_comprobante_del_mismo_correo_coexisten():
|
||||
sin_comprobante = seguimiento.registrar_correo("correo-1")
|
||||
con_comprobante = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
|
||||
assert sin_comprobante["codigo_seguimiento"] != con_comprobante["codigo_seguimiento"]
|
||||
# y cada uno se sigue recuperando por separado
|
||||
assert seguimiento.registrar_correo("correo-1")["codigo_seguimiento"] == sin_comprobante["codigo_seguimiento"]
|
||||
assert seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)["codigo_seguimiento"] == con_comprobante["codigo_seguimiento"]
|
||||
|
||||
|
||||
def test_obtener():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
assert seguimiento.obtener(r["codigo_seguimiento"])["numero"] == "486"
|
||||
assert seguimiento.obtener("ABR-000000-99999") is None
|
||||
|
||||
|
||||
def test_actualizar_resultado_guarda_estado_observaciones_y_doc_entry():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
codigo = r["codigo_seguimiento"]
|
||||
|
||||
seguimiento.actualizar_resultado(codigo, "rechazado", observaciones=[{"codigo": "precio", "detalle": "ñandú"}])
|
||||
rechazado = seguimiento.obtener(codigo)
|
||||
assert rechazado["estado"] == "rechazado"
|
||||
assert rechazado["observaciones"] == [{"codigo": "precio", "detalle": "ñandú"}] # JSON de ida y vuelta
|
||||
|
||||
seguimiento.actualizar_resultado(codigo, "aceptado", doc_entry=2021)
|
||||
aceptado = seguimiento.obtener(codigo)
|
||||
assert (aceptado["estado"], aceptado["doc_entry"], aceptado["observaciones"]) == ("aceptado", 2021, None)
|
||||
|
||||
|
||||
def test_actualizar_resultado_valida_estado_y_codigo():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
with pytest.raises(ValueError):
|
||||
seguimiento.actualizar_resultado(r["codigo_seguimiento"], "inventado")
|
||||
with pytest.raises(LookupError):
|
||||
seguimiento.actualizar_resultado("ABR-000000-99999", "aceptado")
|
||||
|
||||
|
||||
def test_registrar_notificacion():
|
||||
r = seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
codigo = r["codigo_seguimiento"]
|
||||
assert r["notificacion_fecha"] is None
|
||||
|
||||
seguimiento.registrar_notificacion(codigo, "fallida", error="Graph 503")
|
||||
fallida = seguimiento.obtener(codigo)
|
||||
assert (fallida["notificacion_estado"], fallida["notificacion_error"]) == ("fallida", "Graph 503")
|
||||
assert fallida["notificacion_fecha"] is not None
|
||||
|
||||
seguimiento.registrar_notificacion(codigo, "enviada")
|
||||
assert seguimiento.obtener(codigo)["notificacion_error"] is None
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
seguimiento.registrar_notificacion(codigo, "inventado")
|
||||
with pytest.raises(LookupError):
|
||||
seguimiento.registrar_notificacion("ABR-000000-99999", "enviada")
|
||||
|
||||
|
||||
def test_init_db_es_idempotente_y_conserva_los_datos():
|
||||
seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)
|
||||
database_utilities.save_emails([{"id": "correo-1", "fecha": "2026-10-19 10:00:00", "sender": "a@b.c",
|
||||
"subject": "s", "has_attach": True}])
|
||||
|
||||
database_utilities.init_db() # segunda vez: no debe borrar nada
|
||||
|
||||
assert database_utilities.get_existing_ids(["correo-1"]) == ["correo-1"]
|
||||
assert seguimiento.registrar_comprobante("correo-1", *COMPROBANTE)["veces_recibido"] == 1
|
||||
|
||||
|
||||
def test_la_tabla_emails_existente_no_se_modifica():
|
||||
"""Una BD anterior (solo con ``emails``) debe actualizarse al esquema nuevo sin perder correos."""
|
||||
conn = sqlite3.connect(database_utilities.DB_NAME)
|
||||
conn.execute("DROP TABLE comprobantes")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
database_utilities.init_db()
|
||||
|
||||
assert seguimiento.registrar_correo("correo-1")["codigo_seguimiento"].startswith("ABR-")
|
||||
Reference in new issue
Block a user