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:
GeorgeandClaude Sonnet 5.5 committed 2026-10-20 22:03:38 -05:00
1 parent 9d295d6485
commit 297cfde0e1
9 files changed
+563 -6

No files matched your search

+78
View File
@@ -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