fix/de los tickets T2026-05-027, T2025-09-004 y T2025-09-056

This commit is contained in:
2026-06-15 11:18:58 -06:00
parent 7644446267
commit 23ed52c78a
29 changed files with 2992 additions and 987 deletions

View File

@@ -224,3 +224,275 @@ class BulkCreateDocumentReplaceTests(APITestCase):
self.assertIn(response.status_code, [status.HTTP_200_OK, status.HTTP_207_MULTI_STATUS, status.HTTP_201_CREATED])
data = response.json()
self.assertGreaterEqual(data.get("already_existing_count", 0), 1)
# ---------------------------------------------------------------------------
# Tests del comando fix_partidas_error
# Una partida descargado=True solo es válida si alguno de sus documentos
# contiene consultarPartidaRespuesta sin tieneError=true. Partidas que solo
# tienen el REQUEST (o errores) deben volver a descargado=False.
# ---------------------------------------------------------------------------
from io import StringIO
from types import SimpleNamespace
from django.core.management import call_command
from django.test import TestCase
XML_RESPUESTA_VALIDA = (
"<?xml version='1.0' encoding='UTF-8'?>"
'<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body>'
'<ns9:consultarPartidaRespuesta xmlns:ns9="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida">'
"<tieneError>false</tieneError><ns9:partida/></ns9:consultarPartidaRespuesta>"
"</S:Body></S:Envelope>"
)
XML_ERROR_VUCEM = (
"<?xml version='1.0' encoding='UTF-8'?>"
'<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"><S:Body>'
'<ns9:consultarPartidaRespuesta xmlns:ns9="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida">'
"<tieneError>true</tieneError></ns9:consultarPartidaRespuesta>"
"</S:Body></S:Envelope>"
)
XML_ECO_REQUEST = (
"<?xml version='1.0' encoding='UTF-8'?>"
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"'
' xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpartida"><soapenv:Body>'
"<con:consultarPartidaPeticion><con:peticion/></con:consultarPartidaPeticion>"
"</soapenv:Body></soapenv:Envelope>"
)
class _FakeMinioObject:
"""Simula el objeto retornado por minio get_object."""
def __init__(self, content):
self._content = content
def read(self):
return self._content
def close(self):
pass
def release_conn(self):
pass
class FixPartidasErrorCommandTests(TestCase):
PED_APP = "24-01-3420-1234567"
def setUp(self):
from api.customs.models import Partida
from api.record.models import DocumentType
self.licencia = Licencia.objects.create(nombre="LicFixPartidas", almacenamiento=100)
self.org = Organizacion.objects.create(
nombre="OrgFixPartidas", licencia=self.licencia, is_active=True, is_verified=True
)
# Pedimento VÁLIDO (no malformado): el comando ya no se limita a malformados
self.pedimento = Pedimento.objects.create(
organizacion=self.org,
pedimento="1234567",
pedimento_app=self.PED_APP,
aduana="034",
patente="3420",
numero_operacion="12345678",
)
self.partida = Partida.objects.create(
pedimento=self.pedimento,
organizacion=self.org,
numero_partida=1,
descargado=True,
)
self.type_resp = DocumentType.objects.get_or_create(id=1, defaults={"nombre": "XML"})[0]
self.type_req = DocumentType.objects.get_or_create(id=17, defaults={"nombre": "PT Request"})[0]
self.type_err = DocumentType.objects.get_or_create(id=18, defaults={"nombre": "PT Error"})[0]
# Storage simulado: dict path -> bytes
self.storage = {}
patcher = patch("api.customs.management.commands.fix_partidas_error.minio_client")
self.minio = patcher.start()
self.addCleanup(patcher.stop)
self.minio._bucket_name = "test-bucket"
self.minio.file_exists.side_effect = lambda name: name in self.storage
self.minio._client.get_object.side_effect = (
lambda bucket, name: _FakeMinioObject(self.storage[name])
)
self.minio.upload_file.side_effect = (
lambda name, file_data=None, content_type=None: self.storage.__setitem__(
name, file_data.read()
)
)
self.minio.delete_file.side_effect = lambda name: self.storage.pop(name, None)
def _doc(self, filename, doc_type, content=None):
from api.record.models import Document
path = f"org/{self.PED_APP}/{filename}"
doc = Document.objects.create(
organizacion=self.org,
pedimento=self.pedimento,
document_type=doc_type,
archivo=path,
size=100,
extension="xml",
)
if content is not None:
self.storage[path] = content.encode("utf-8")
return doc
def _run(self, **kwargs):
out = StringIO()
call_command("fix_partidas_error", stdout=out, stderr=StringIO(), **kwargs)
return out.getvalue()
def test_partida_solo_request_se_marca_no_descargada(self):
"""El caso reportado: descargado=True pero solo existe el XML del REQUEST."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertFalse(self.partida.descargado)
def test_partida_sin_documentos_se_marca_no_descargada(self):
"""descargado=True sin ningún documento tampoco es una descarga real."""
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertFalse(self.partida.descargado)
def test_partida_con_respuesta_valida_permanece_descargada(self):
"""Con consultarPartidaRespuesta sin error la partida no se toca."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_RESPUESTA_VALIDA)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertTrue(self.partida.descargado)
def test_doc_con_error_vucem_se_renombra_y_marca_no_descargada(self):
"""tieneError=true: doc → type 18 con sufijo _ERROR y partida → False."""
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
old_path = doc.archivo.name
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
doc.refresh_from_db()
self.assertFalse(self.partida.descargado)
self.assertEqual(doc.document_type_id, 18)
self.assertTrue(doc.archivo.name.endswith(f"vu_PT_{self.PED_APP}_1_ERROR.xml"))
self.assertTrue(doc.vu)
self.assertNotIn(old_path, self.storage)
self.assertIn(doc.archivo.name, self.storage)
def test_eco_de_request_guardado_como_respuesta_se_reclasifica(self):
"""Un eco de consultarPartidaPeticion guardado como respuesta se
reclasifica a type 17 sin chocar con el REQUEST real existente."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ECO_REQUEST)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
doc.refresh_from_db()
self.assertFalse(self.partida.descargado)
self.assertEqual(doc.document_type_id, 17)
# El nombre sin índice ya lo usa el REQUEST real → debe ir con _1
self.assertTrue(doc.archivo.name.endswith(f"vu_PT_{self.PED_APP}_1_REQUEST_1.xml"))
def test_doc_ausente_sin_canario_no_cambia_partida(self):
"""Archivo ausente y NINGÚN archivo del pedimento en storage: posible
storage equivocado (p. ej. dev) → sin cambios."""
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, content=None)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertTrue(self.partida.descargado)
def test_registro_fantasma_con_storage_real_se_marca_no_descargada(self):
"""Document type 1 en BD sin archivo en storage, pero el REQUEST sí
existe físicamente (canario): el storage es el correcto, el registro es
fantasma → la partida no tiene XML de partida → descargado=False."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
fantasma = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, content=None)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
fantasma.refresh_from_db()
self.assertFalse(self.partida.descargado)
# El registro fantasma se reporta pero no se modifica ni se borra
self.assertEqual(fantasma.document_type_id, 1)
def test_storage_inaccesible_no_cambia_partida(self):
"""Excepción al consultar storage (conexión caída): sin cambios."""
self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
self.minio.file_exists.side_effect = Exception("connection refused")
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertTrue(self.partida.descargado)
def test_naming_legacy_valida_partida(self):
"""Documentos con nomenclatura legacy (partida al final) también validan."""
self._doc("vu_PT_010Imp_034_3420_1234567_1.xml", self.type_resp, XML_RESPUESTA_VALIDA)
self._run(pedimento=str(self.pedimento.id))
self.partida.refresh_from_db()
self.assertTrue(self.partida.descargado)
def test_dry_run_no_modifica(self):
"""--dry-run reporta pero no toca BD ni storage."""
doc = self._doc(f"vu_PT_{self.PED_APP}_1.xml", self.type_resp, XML_ERROR_VUCEM)
self._run(pedimento=str(self.pedimento.id), dry_run=True)
self.partida.refresh_from_db()
doc.refresh_from_db()
self.assertTrue(self.partida.descargado)
self.assertEqual(doc.document_type_id, 1)
self.assertIn(doc.archivo.name, self.storage)
def test_universo_general_incluye_pedimentos_validos(self):
"""Sin --pedimento ni --solo-malformados también procesa pedimentos bien formados."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
self._run()
self.partida.refresh_from_db()
self.assertFalse(self.partida.descargado)
def test_solo_malformados_excluye_pedimentos_validos(self):
"""Con --solo-malformados un pedimento bien formado no se procesa."""
self._doc(f"vu_PT_{self.PED_APP}_1_REQUEST.xml", self.type_req, XML_ECO_REQUEST)
self._run(solo_malformados=True)
self.partida.refresh_from_db()
self.assertTrue(self.partida.descargado)
def test_no_confunde_partida_1_con_11(self):
"""La asignación por nombre no debe mezclar partida 1 con partida 11."""
from api.customs.management.commands.fix_partidas_error import Command
docs = [
SimpleNamespace(id=1, document_type_id=1, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_1.xml")),
SimpleNamespace(id=2, document_type_id=17, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_1_REQUEST.xml")),
SimpleNamespace(id=3, document_type_id=1, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_11.xml")),
SimpleNamespace(id=4, document_type_id=17, archivo=SimpleNamespace(name=f"org/x/vu_PT_{self.PED_APP}_11_REQUEST.xml")),
]
cmd = Command()
ids_p1 = {d.id for d in cmd._docs_de_partida(docs, self.PED_APP, 1)}
ids_p11 = {d.id for d in cmd._docs_de_partida(docs, self.PED_APP, 11)}
self.assertEqual(ids_p1, {1, 2})
self.assertEqual(ids_p11, {3, 4})