359 lines
14 KiB
Python
359 lines
14 KiB
Python
import base64
|
|
import re
|
|
import logging
|
|
import os
|
|
from typing import Any, Dict, Optional
|
|
import xml.etree.ElementTree as ET
|
|
from fastapi import HTTPException
|
|
|
|
from utils.helpers import soap_error
|
|
from .controllers import edocs_rest_controller, edocs_vu_controller
|
|
from ..common import create_service_response, create_error_response
|
|
|
|
# Logger para el módulo
|
|
logger = logging.getLogger("app.api")
|
|
|
|
|
|
|
|
# --- FUNCIONES AUXILIARES ---
|
|
|
|
|
|
def _get_file_name(**kwargs) -> str:
|
|
pedimento = kwargs.get('pedimento', {})
|
|
pedimento_app = pedimento.get('pedimento_app', 'N/A')
|
|
idEDocument = kwargs['edoc'].get('numero_edocument', 'N/A')
|
|
return f"vu_ED_{pedimento_app}_{idEDocument}.pdf"
|
|
|
|
# --- FUNCIONES DE SERVICIO ---
|
|
|
|
async def obtener_edoc(**kwargs):
|
|
|
|
credencial = kwargs.get('credencial', {})
|
|
usuario = credencial.get('user', '')
|
|
password = credencial.get('password', '')
|
|
doc = kwargs.get('edoc', {})
|
|
numero_documento = kwargs['edoc'].get('numero_edocument', '')
|
|
soap_headers = {
|
|
'Content-Type': 'text/xml; charset=utf-8',
|
|
'SOAPAction': 'http://tempuri.org/IServicioEdocument/GetDocumento'
|
|
}
|
|
soap_xml = edocs_vu_controller.generate_edocument_template(username=usuario, password=password, idEDocument=numero_documento)
|
|
|
|
# Enviar documento a EFC
|
|
try:
|
|
pedimento_efc = kwargs.get('pedimento', {})
|
|
pedimento_app = pedimento_efc.get('pedimento_app', 'N/A')
|
|
organizacion_efc = pedimento_efc.get('organizacion', None)
|
|
pedimento_id_efc = pedimento_efc.get("id", None)
|
|
|
|
file_name_request = f"VU_ED_{pedimento_app}_{numero_documento}_REQUEST.xml"
|
|
|
|
document_response = await edocs_rest_controller.post_or_update_document(
|
|
soap_response=soap_xml,
|
|
organizacion=organizacion_efc,
|
|
pedimento=pedimento_id_efc,
|
|
file_name=file_name_request,
|
|
document_type=21,
|
|
identifier=numero_documento,
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error al enviar documento request: {e}")
|
|
|
|
|
|
response = await edocs_vu_controller.make_request_async(
|
|
"Ventanilla-HA/ServicioEdocument/ServicioEdocument.svc",
|
|
data=soap_xml,
|
|
headers=soap_headers
|
|
)
|
|
# Validar respuesta del servicio SOAP
|
|
if response is None:
|
|
logger.error("No se obtuvo respuesta del servicio SOAP")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al contactar el servicio SOAP",
|
|
errors=["No se obtuvo respuesta del servicio"],
|
|
metadata={
|
|
"edoc_number": numero_documento,
|
|
"username": usuario
|
|
}
|
|
)
|
|
)
|
|
|
|
if response.status_code != 200:
|
|
logger.error(f"Error en la solicitud SOAP: {response.status_code}")
|
|
raise HTTPException(
|
|
status_code=response.status_code,
|
|
detail=create_error_response(
|
|
message="Error en la solicitud SOAP",
|
|
errors=[f"Código de estado: {response.status_code}"],
|
|
data={"soap_response": response.text[:500]},
|
|
metadata={
|
|
"status_code": response.status_code,
|
|
"edoc_number": numero_documento
|
|
}
|
|
)
|
|
)
|
|
|
|
if soap_error(response):
|
|
logger.error("Respuesta SOAP contiene error de VUCEM")
|
|
_pedimento_efc = kwargs.get('pedimento', {})
|
|
_file_name_error = f"VU_ED_{_pedimento_efc.get('pedimento_app', 'N/A')}_{numero_documento}_RESPONSE_ERROR.xml"
|
|
logger.info(f"Guardando RESPONSE_ERROR doc_type=22: file={_file_name_error}, organizacion={_pedimento_efc.get('organizacion')}, pedimento={_pedimento_efc.get('id')}")
|
|
_doc_result = await edocs_rest_controller.post_or_update_document(
|
|
soap_response=response.text,
|
|
organizacion=_pedimento_efc.get('organizacion'),
|
|
pedimento=_pedimento_efc.get('id'),
|
|
file_name=_file_name_error,
|
|
document_type=22,
|
|
identifier=numero_documento,
|
|
)
|
|
if _doc_result is None:
|
|
logger.error("post_or_update_document retornó None para RESPONSE_ERROR doc_type=22 — archivo físico sin registro en BD")
|
|
else:
|
|
logger.info(f"RESPONSE_ERROR registrado en BD: id={_doc_result.get('id')}, document_type={_doc_result.get('document_type')}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error en la respuesta del servicio SOAP",
|
|
errors=["La respuesta contiene un error de VUCEM"],
|
|
data={"soap_response": response.text[:500]},
|
|
metadata={"edoc_number": numero_documento}
|
|
)
|
|
)
|
|
|
|
try:
|
|
edoc_base64 = extract_pdf_bytes_from_xml_content(response.text)
|
|
except Exception as ve:
|
|
logger.error(f"Error extrayendo contenido del XML: {ve}")
|
|
_pedimento_efc = kwargs.get('pedimento', {})
|
|
_file_name_error = f"VU_ED_{_pedimento_efc.get('pedimento_app', 'N/A')}_{numero_documento}_RESPONSE_ERROR.xml"
|
|
logger.info(f"Guardando RESPONSE_ERROR doc_type=22 (parse error): file={_file_name_error}")
|
|
_doc_result = await edocs_rest_controller.post_or_update_document(
|
|
soap_response=response.text,
|
|
organizacion=_pedimento_efc.get('organizacion'),
|
|
pedimento=_pedimento_efc.get('id'),
|
|
file_name=_file_name_error,
|
|
document_type=22,
|
|
identifier=numero_documento,
|
|
)
|
|
if _doc_result is None:
|
|
logger.error("post_document retornó None para RESPONSE_ERROR doc_type=22 (parse error)")
|
|
else:
|
|
logger.info(f"RESPONSE_ERROR registrado en BD: id={_doc_result.get('id')}, document_type={_doc_result.get('document_type')}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al procesar la respuesta SOAP",
|
|
errors=[str(ve)],
|
|
metadata={"edoc_number": numero_documento}
|
|
)
|
|
)
|
|
|
|
if edoc_base64 is None:
|
|
logger.error("No se pudo extraer el documento de la respuesta SOAP")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al extraer el documento",
|
|
errors=["No se pudo encontrar el documento en la respuesta SOAP"],
|
|
metadata={"edoc_number": numero_documento}
|
|
)
|
|
)
|
|
|
|
pdf_bytes = edoc_base64['pdf_bytes']
|
|
if not pdf_bytes:
|
|
logger.error("No se pudo decodificar el documento PDF")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al decodificar el documento",
|
|
errors=["El contenido del documento está vacío o es inválido"],
|
|
metadata={
|
|
"edoc_number": numero_documento,
|
|
"has_cadena_original": bool(edoc_base64.get('cadena_original')),
|
|
"has_sello_digital": bool(edoc_base64.get('sello_digital'))
|
|
}
|
|
)
|
|
)
|
|
|
|
# Validar formato PDF
|
|
if not pdf_bytes.startswith(b'%PDF'):
|
|
logger.error("El contenido decodificado no es un PDF válido")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="El documento recibido no es un PDF válido",
|
|
errors=["El contenido no tiene el formato PDF esperado"],
|
|
metadata={
|
|
"edoc_number": numero_documento,
|
|
"content_start": str(pdf_bytes[:20])
|
|
}
|
|
)
|
|
)
|
|
|
|
pedimento = kwargs.get('pedimento', {})
|
|
numero_documento = kwargs['edoc'].get('numero_edocument', '')
|
|
_file_name = _get_file_name(**kwargs)
|
|
|
|
organizacion = pedimento.get("organizacion", None)
|
|
pedimento_id = pedimento.get("id", None)
|
|
|
|
# No guardaremos el archivo localmente por seguridad
|
|
logger.debug(f"Procesando documento {numero_documento} para pedimento {pedimento_id}")
|
|
|
|
rest_response = await edocs_rest_controller.post_or_update_document(
|
|
binary_content=pdf_bytes,
|
|
organizacion=organizacion,
|
|
pedimento=pedimento_id,
|
|
file_name=_file_name,
|
|
document_type=5,
|
|
identifier=numero_documento,
|
|
)
|
|
|
|
print(f"rest_response >>>> {rest_response}")
|
|
|
|
if rest_response is None:
|
|
logger.error("Error al enviar el documento a la API interna")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al guardar el documento en el sistema",
|
|
errors=["No se pudo enviar el documento a la API interna"],
|
|
metadata={
|
|
"file_name": _file_name,
|
|
"edoc_number": numero_documento
|
|
}
|
|
)
|
|
)
|
|
|
|
if rest_response.get("id") is None:
|
|
logger.error("Respuesta de API interna sin ID válido")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=create_error_response(
|
|
message="Error al procesar la respuesta del sistema",
|
|
errors=["La respuesta de la API no contiene un ID válido"],
|
|
data={"api_response": rest_response}
|
|
)
|
|
)
|
|
|
|
logger.info("Documento enviado, actualizando status de Edoc")
|
|
|
|
try:
|
|
edoc_status_response = await change_edocument_status(
|
|
edoc=doc,
|
|
status=True,
|
|
pedimento=pedimento
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Error al actualizar estado del documento: {e}")
|
|
# No fallamos aquí porque el documento ya se guardó exitosamente
|
|
|
|
logger.info(f"E-document {numero_documento} procesado exitosamente")
|
|
|
|
return create_service_response(
|
|
message=f"E-document {numero_documento} procesado exitosamente",
|
|
data={
|
|
"document_response": rest_response,
|
|
"file_name": _file_name,
|
|
"numero_documento": numero_documento,
|
|
"edoc_update_response": edoc_status_response if edoc_status_response else None
|
|
},
|
|
metadata={
|
|
"document_type": 5,
|
|
"pedimento_app": pedimento.get('pedimento_app'),
|
|
"organizacion": organizacion,
|
|
"content_type": "application/pdf",
|
|
"has_cadena_original": bool(edoc_base64.get('cadena_original')),
|
|
"has_sello_digital": bool(edoc_base64.get('sello_digital'))
|
|
}
|
|
)
|
|
|
|
|
|
async def change_edocument_status(edoc: dict, status: bool, pedimento: dict):
|
|
# estaba acualizando mal el status de descarga, actualizaba otro campo que no le correspondia
|
|
data = {
|
|
"id": edoc.get("id"),
|
|
"edocument_descargado": status,
|
|
"edocument_estado": "descargado" if status else "pendiente",
|
|
"numero_edocument": edoc.get("numero_edocument"),
|
|
"pedimento": pedimento.get("id"),
|
|
"organizacion": pedimento.get("organizacion"),
|
|
}
|
|
|
|
response = await edocs_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
|
|
|
|
# Nunca reportar éxito si el estatus no quedó persistido (T2026-05-027)
|
|
if response is None:
|
|
logger.error(f"No se pudo actualizar el estatus del EDocument {edoc.get('numero_edocument')} en la API")
|
|
raise Exception(f"Fallo al actualizar el estatus del EDocument {edoc.get('numero_edocument')}")
|
|
|
|
return response
|
|
|
|
|
|
async def marcar_error_edocument(edoc: dict, pedimento: dict, mensaje: str, definitivo: bool = False):
|
|
"""
|
|
Reporta un fallo de descarga al registro de negocio (T2026-05-027).
|
|
|
|
- definitivo=False (fallo transitorio): solo registra ultimo_error; el registro
|
|
permanece 'pendiente' y el tope de intentos automáticos del backend gobierna
|
|
la transición a 'error'.
|
|
- definitivo=True (fallo permanente): transiciona de inmediato a 'error';
|
|
queda fuera del ciclo automático, solo reproceso manual o reset.
|
|
"""
|
|
data = {
|
|
"id": edoc.get("id"),
|
|
"ultimo_error": (mensaje or "Error de descarga en VUCEM")[:2000],
|
|
"numero_edocument": edoc.get("numero_edocument"),
|
|
"pedimento": pedimento.get("id"),
|
|
"organizacion": pedimento.get("organizacion"),
|
|
}
|
|
if definitivo:
|
|
data["edocument_estado"] = "error"
|
|
response = await edocs_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
|
|
if response is None:
|
|
logger.error(f"No se pudo registrar el error del EDocument {edoc.get('numero_edocument')} en la API")
|
|
return response
|
|
|
|
|
|
NS = {
|
|
's': 'http://schemas.xmlsoap.org/soap/envelope/',
|
|
't': 'http://tempuri.org/',
|
|
'i': 'http://www.w3.org/2001/XMLSchema-instance'
|
|
}
|
|
|
|
# mejorar la extraccion de seccion File
|
|
def extract_pdf_bytes_from_xml_content(xml_content: str):
|
|
root = ET.fromstring(xml_content)
|
|
|
|
errores = root.find('.//t:Errores', NS)
|
|
tiene_error = root.find('.//t:TieneError', NS)
|
|
|
|
if tiene_error is not None and tiene_error.text == 'true':
|
|
err_msg = errores.text if errores is not None else 'Error desconocido'
|
|
raise Exception(f"VUCEM informa error: {err_msg}")
|
|
|
|
file_elem = root.find('.//t:File', NS)
|
|
if file_elem is None or file_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') == 'true' or not file_elem.text:
|
|
raise ValueError("No se encontró el tag <File> con contenido válido.")
|
|
|
|
base64_data = file_elem.text.strip().replace('\n', '').replace('\r', '')
|
|
pdf_bytes = base64.b64decode(base64_data)
|
|
|
|
# Extraer CadenaOriginal y SelloDigital con namespaces
|
|
cadena_original = None
|
|
sello_digital = None
|
|
cadena_elem = root.find('.//t:CadenaOriginal', NS)
|
|
if cadena_elem is not None and cadena_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') != 'true':
|
|
cadena_original = cadena_elem.text.strip() if cadena_elem.text else None
|
|
sello_elem = root.find('.//t:SelloDigital', NS)
|
|
if sello_elem is not None and sello_elem.get('{http://www.w3.org/2001/XMLSchema-instance}nil') != 'true':
|
|
sello_digital = sello_elem.text.strip() if sello_elem.text else None
|
|
|
|
return {
|
|
"pdf_bytes": pdf_bytes,
|
|
"cadena_original": cadena_original,
|
|
"sello_digital": sello_digital
|
|
} |