Primera version estable de microservicios

This commit is contained in:
2025-07-28 11:04:18 -06:00
parent 42a564cd74
commit 5f58fabcfe
37 changed files with 5079 additions and 0 deletions

0
utils/__init__.py Normal file
View File

884
utils/peticiones.py Normal file
View File

@@ -0,0 +1,884 @@
import logging
logger = logging.getLogger("app.api")
from fastapi import HTTPException
from controllers.RESTController import rest_controller
from typing import Dict, Any
import xml.etree.ElementTree as ET
import base64
import re
from schemas.serviceSchema import ServiceBaseSchema
logger = logging.getLogger(__name__)
from controllers.XMLController import xml_controller
def validate_pedimento_data(response_service: Dict[str, Any], credenciales: Dict[str, Any]) -> tuple: # Testeado
"""
Valida y extrae los datos necesarios para la petición SOAP.
Args:
response_service: Respuesta del servicio con datos del pedimento
credenciales: Credenciales VUCEM
Returns:
tuple: (username, password, aduana, patente, pedimento)
Raises:
HTTPException: Si faltan datos requeridos
"""
# Validar credenciales
username = credenciales.get('usuario')
password = credenciales.get('password')
if not username or not password:
logger.error("Credenciales VUCEM incompletas")
raise HTTPException(status_code=400, detail="Credenciales VUCEM incompletas")
# Validar datos del pedimento
pedimento_data = response_service.get('pedimento', {})
aduana = pedimento_data.get('aduana')
patente = pedimento_data.get('patente')
pedimento = pedimento_data.get('pedimento')
numero_operacion = pedimento_data.get('numero_operacion')
if not all([aduana, patente, pedimento]):
logger.error(f"Datos del pedimento incompletos - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
raise HTTPException(status_code=400, detail="Datos del pedimento incompletos")
return username, password, aduana, patente, pedimento, numero_operacion
def extract_acuse_documento_from_soap(soap_response_text): # Testeado
"""
Extrae el contenido del tag <acuseDocumento> de la respuesta SOAP multipart.
Args:
soap_response_text (str): Contenido de la respuesta SOAP
Returns:
str: Contenido Base64 del acuseDocumento o None si no se encuentra
"""
try:
# Primero, extraer la parte XML del contenido multipart
xml_start = soap_response_text.find('<?xml')
if xml_start == -1:
logger.error("No se encontró contenido XML en la respuesta SOAP")
return None
# Extraer solo la parte XML
xml_content = soap_response_text[xml_start:]
# Si hay más contenido multipart después, cortarlo
boundary_end = xml_content.find('--uuid:')
if boundary_end != -1:
xml_content = xml_content[:boundary_end]
# Parsear el XML
root = ET.fromstring(xml_content.strip())
# Buscar el elemento acuseDocumento con namespaces
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns3': 'http://www.ventanillaunica.gob.mx/ws/consulta/acuses/'
}
# Buscar el elemento acuseDocumento
acuse_elemento = root.find('.//ns3:responseConsultaAcuses/acuseDocumento', namespaces)
if acuse_elemento is None:
# Intentar sin namespace
acuse_elemento = root.find('.//acuseDocumento')
if acuse_elemento is not None and acuse_elemento.text:
return acuse_elemento.text.strip()
else:
logger.error("No se encontró el tag <acuseDocumento> o está vacío")
return None
except ET.ParseError as e:
logger.error(f"Error parseando XML: {e}")
return None
except Exception as e:
logger.error(f"Error extrayendo acuseDocumento: {e}")
return None
def extract_pdf_bytes_from_xml(xml_path):
tree = ET.parse(xml_path)
root = tree.getroot()
# Busca el tag <File> (ajusta el namespace si es necesario)
file_elem = root.find('.//File')
if file_elem is not None and file_elem.text:
# Limpia el contenido base64
base64_data = file_elem.text.strip().replace('\n', '').replace('\r', '')
pdf_bytes = base64.b64decode(base64_data)
cadena_original = None
sello_digital = None
# Buscar CadenaOriginal y SelloDigital en el XML
cadena_elem = root.find('.//CadenaOriginal')
if cadena_elem is not None and cadena_elem.text:
cadena_original = cadena_elem.text.strip()
sello_elem = root.find('.//SelloDigital')
if sello_elem is not None and sello_elem.text:
sello_digital = sello_elem.text.strip()
return {
"pdf_bytes": pdf_bytes,
"cadena_original": cadena_original,
"sello_digital": sello_digital
}
return pdf_bytes
else:
raise ValueError("No se encontró el tag <File> con contenido válido.")
def decode_acuse_base64_content(base64_content): # Testeado
"""
Decodifica el contenido Base64 del acuse y limpia caracteres especiales.
Args:
base64_content (str): Contenido codificado en Base64
Returns:
bytes: Contenido decodificado o None si hay error
"""
try:
# Limpiar el contenido Base64 de manera exhaustiva
cleaned_content = base64_content
# Remover entidades HTML/XML como &#xd;, &#xa;, etc.
cleaned_content = re.sub(r'&#x[0-9a-fA-F]+;', '', cleaned_content)
cleaned_content = re.sub(r'&#[0-9]+;', '', cleaned_content)
# Remover espacios en blanco, saltos de línea, etc.
cleaned_content = re.sub(r'[\s\n\r\t]', '', cleaned_content)
# Remover caracteres no válidos para Base64
cleaned_content = re.sub(r'[^A-Za-z0-9+/=]', '', cleaned_content)
logger.info(f"Contenido Base64 limpiado: {len(cleaned_content)} caracteres")
# Agregar padding si es necesario
missing_padding = len(cleaned_content) % 4
if missing_padding:
cleaned_content += '=' * (4 - missing_padding)
logger.info(f"Padding agregado: {4 - missing_padding} caracteres '='")
# Decodificar Base64
decoded_content = base64.b64decode(cleaned_content)
logger.info(f"Contenido decodificado exitosamente: {len(decoded_content)} bytes")
return decoded_content
except Exception as e:
logger.error(f"Error decodificando Base64: {e}")
# Intentar con validación estricta deshabilitada
try:
logger.info("Intentando decodificación con validación relajada...")
decoded_content = base64.b64decode(cleaned_content, validate=False)
logger.info("¡Decodificación exitosa con validación relajada!")
return decoded_content
except Exception as e2:
logger.error(f"Error también con validación relajada: {e2}")
return None
def soap_error(soap_response): # Testeado
"""
Verifica si la respuesta SOAP no contiene errores.
Args:
soap_response: Respuesta del servicio SOAP
Returns:
bool: True si no hay errores, False en caso contrario
"""
if '<ns3:tieneError>true</ns3:tieneError>' in soap_response.text:
return True
# Aquí podrías agregar más lógica para verificar errores específicos en el XML
return False
async def get_soap_pedimento_completo(credenciales, response_service, soap_controller): # Testeado
"""
Procesa la petición SOAP para obtener el pedimento completo y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, _ = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
# Generar template SOAP
soap_xml = soap_controller.generate_pedimento_completo_template(
username=username,
password=password,
aduana=aduana,
patente=patente,
pedimento=pedimento
)
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
soap_response = await soap_controller.make_request_async(
"ventanilla-ws-pedimentos/ConsultarPedimentoCompletoService?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
data = xml_controller.extract_data(soap_response.text)
remesas = 1 if data.get('remesas', 0) else 2
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = data.get('numero_partidas', 0)
tipo_operacion = data.get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_PC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
# Enviar el documento XML como respuesta
document_response = await rest_controller.post_document(
soap_response=soap_response,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=2
)
data['organizacion'] = response_service['organizacion']
data['id'] = response_service['pedimento']['id']
return {
"servicio": response_service,
"documento": document_response,
"xml_content": data
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_pedimento_completo: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar pedimento completo: {str(e)}")
async def get_soap_remesas(credenciales, response_service, soap_controller): # Testeado
"""
Procesa la petición SOAP para obtener remesas y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
# Generar template SOAP
soap_xml = soap_controller.generate_remesas_template(
username=username,
password=password,
aduana=aduana,
patente=patente,
pedimento=pedimento,
numero_operacion=numero_operacion
)
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
soap_response = await soap_controller.make_request_async(
"ventanilla-ws-pedimentos/ConsultarRemesasService?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
# data = xml_controller.extract_data(soap_response.text)
# # Enviar el documento XML como respuesta
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_RM_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
document_response = await rest_controller.post_document(
soap_response=soap_response,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=3
)
return {
"servicio": response_service,
"documento": document_response
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_remesas: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar remesas: {str(e)}")
async def get_soap_partidas(credenciales, response_service, soap_controller, partida): # Testeado
"""
Procesa la petición SOAP para obtener partidas de un pedimento y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
partida: Número de partida a consultar
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}, Partida: {partida}")
# Generar template SOAP
soap_xml = soap_controller.generate_partidas_template(
username=username,
password=password,
aduana=aduana,
patente=patente,
pedimento=pedimento,
numero_operacion=numero_operacion,
partida=partida
)
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
soap_response = await soap_controller.make_request_async(
"ventanilla-ws-pedimentos/ConsultarPartidaService?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
# partida = get_partida_data(soap_response.text)
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_PT_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{partida}.xml"
# Aqui entra proceso de alonso
# respuesta = scraping_partida(xml)
# 1. Leer XML
# 2. Obtener Datos del XML
# 3. Subir los datos a la base datos
# 4. Generar una respues
# Enviar el documento XML como respuesta
document_response = await rest_controller.post_document(
soap_response=soap_response,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=1
)
return {
"servicio": response_service,
"documento": document_response
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_partidas: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar partidas: {str(e)}")
async def get_soap_acuse(credenciales, response_service, soap_controller, edocument, idx): # Testeado
"""
Procesa la petición SOAP para obtener el acuse de un pedimento y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
# Generar template SOAP
soap_xml = soap_controller.generate_acuse_template(
username=username,
password=password,
idEDocument=edocument['numero_edocument']
)
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseEdocument',# AcuseCove
'Accept-Encoding': 'gzip,deflate',
}
soap_response = await soap_controller.make_request_async(
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
# Extraer contenido Base64 del acuse
logger.info("Extrayendo documento binario del acuse...")
acuse_base64 = extract_acuse_documento_from_soap(soap_response.text)
if not acuse_base64:
logger.error("No se pudo extraer el contenido del acuseDocumento")
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
# Decodificar contenido Base64
logger.info("Decodificando contenido Base64...")
pdf_bytes = decode_acuse_base64_content(acuse_base64)
if not pdf_bytes:
logger.error("No se pudo decodificar el contenido Base64 del acuse")
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento del acuse")
# Verificar que es un PDF válido
if not pdf_bytes.startswith(b'%PDF'):
logger.warning("El contenido decodificado no parece ser un PDF válido")
# Continuar de todos modos, podría ser otro tipo de documento
# Generar nombre del archivo
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_AC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
# Enviar el documento PDF usando binary_content
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
document_response = await rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=4
)
return {
"servicio": response_service,
"documento": document_response
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_acuse: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse: {str(e)}")
async def get_soap_acuseCOVE(credenciales, response_service, soap_controller, cove, idx): # Testeado
"""
Procesa la petición SOAP para obtener el acuse COVE de un pedimento y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
# Generar template SOAP
soap_xml = soap_controller.generate_acuse_template(
username=username,
password=password,
idEDocument=cove['numero_cove']
)
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseCove',
'Accept-Encoding': 'gzip,deflate',
}
soap_response = await soap_controller.make_request_async(
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
# Extraer contenido Base64 del acuse
logger.info("Extrayendo documento binario del acuse cove...")
acuse_base64 = extract_acuse_documento_from_soap(soap_response.text)
if not acuse_base64:
logger.error("No se pudo extraer el contenido del acuseDocumento")
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
# Decodificar contenido Base64
logger.info("Decodificando contenido Base64...")
pdf_bytes = decode_acuse_base64_content(acuse_base64)
if not pdf_bytes:
logger.error("No se pudo decodificar el contenido Base64 del acuse")
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento del acuse")
# Verificar que es un PDF válido
if not pdf_bytes.startswith(b'%PDF'):
logger.warning("El contenido decodificado no parece ser un PDF válido")
# Continuar de todos modos, podría ser otro tipo de documento
# Generar nombre del archivo
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_AC_COVE_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
# Enviar el documento PDF usando binary_content
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
document_response = await rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=7
)
return {
"servicio": response_service,
"documento": document_response
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_acuse cove: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse cove: {str(e)}")
async def get_estado_pedimento(credenciales, response_service, soap_controller): # Sin testear
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
# Generar template SOAP
soap_xml = soap_controller.generate_estado_pedimento_template(
username=username,
password=password,
aduana=aduana,
patente=patente,
pedimento=pedimento,
numero_operacion=numero_operacion
)
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
soap_response = await soap_controller.make_request_async(
"webservice-pedimentos-HA/consultarEstadoPedimento?wsdl",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
data = xml_controller.extract_data(soap_response.text)
remesas = 1 if data.get('remesas', 0) else 2
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = data.get('numero_partidas', 0)
tipo_operacion = data.get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_EP_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
# Enviar el documento XML como respuesta
document_response = await rest_controller.post_document(
soap_response=soap_response,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=6
)
data['organizacion'] = response_service['organizacion']
data['id'] = response_service['pedimento']['id']
return {
"servicio": response_service,
"documento": document_response,
"xml_content": data
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_pedimento_completo: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar pedimento completo: {str(e)}")
async def get_soap_edocument(credenciales, response_service, soap_controller, edocument, idx):
"""
Procesa la petición SOAP para obtener el acuse de un pedimento y guarda el documento.
Args:
credenciales: Diccionario con credenciales VUCEM (usuario, password)
response_service: Respuesta del servicio con datos del pedimento
soap_controller: Instancia del controlador SOAP
Returns:
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
Raises:
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
"""
try:
# Extraer credenciales
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
# Generar template SOAP
soap_xml = soap_controller.generate_edocument_template(
username=username,
password=password,
idEDocument=edocument['numero_edocument']
)
# Realizar petición SOAP
logger.info("Realizando petición SOAP...")
# Headers específicos para este servicio SOAP
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://tempuri.org/IServicioEdocument/GetDocumento',
'Accept-Encoding': 'gzip,deflate',
}
soap_response = await soap_controller.make_request(
"Ventanilla-HA/ServicioEdocument/ServicioEdocument.svc",
data=soap_xml,
headers=soap_headers
)
if (soap_response) and (not soap_error(soap_response)):
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
# Extraer contenido Base64 del acuse
logger.info("Extrayendo documento binario del edocument...")
response = extract_pdf_bytes_from_xml(soap_response.text)
pdf_bytes = response.get('pdf_bytes')
# cadena_original = response.get('cadena_original')
# sello_digital = response.get('sello_digital')
if not acuse_base64:
logger.error("No se pudo extraer el contenido del acuseDocumento")
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
# Decodificar contenido Base64
response_edoc = rest_controller.put_edocument(edocument_id=ide, data={
"numero_edocument": edocument['numero_edocument'],
"pedimento": response_service['pedimento'],
# "cadena_original": cadena_original,
# "sello_digital": sello_digital
})
if not pdf_bytes:
logger.error("No se pudo decodificar el contenido Base64 del acuse")
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento del acuse")
# Verificar que es un PDF válido
if not pdf_bytes.startswith(b'%PDF'):
logger.warning("El contenido decodificado no parece ser un PDF válido")
# Continuar de todos modos, podría ser otro tipo de documento
# Generar nombre del archivo
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
patente = response_service['pedimento'].get('patente', 'N/A')
aduana = response_service['pedimento'].get('aduana', 'N/A')
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
_file_name = f"vu_EDC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
# Enviar el documento PDF usando binary_content
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
document_response = await rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=response_service['organizacion'],
pedimento=response_service['pedimento']['id'],
file_name=_file_name,
document_type=5
)
return {
"servicio": response_service,
"documento": document_response
}
else:
logger.error("Error en petición SOAP")
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
except HTTPException:
# Re-lanzar HTTPExceptions sin modificar
raise
except Exception as e:
logger.error(f"Error inesperado en get_acuse: {e}")
import traceback
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse: {str(e)}")

558
utils/servicios.py Normal file
View File

@@ -0,0 +1,558 @@
from core.config import settings
from fastapi import APIRouter, HTTPException
from schemas.pedimentoSchema import PedimentoRequest
from schemas.serviceSchema import ServiceBaseSchema, ServiceRemesaSchema
import asyncio
import logging
logger = logging.getLogger("app.api")
import traceback
from typing import Dict, Any, List, Optional
from contextlib import asynccontextmanager
from controllers.RESTController import rest_controller
from controllers.SOAPController import soap_controller
from utils.peticiones import get_soap_pedimento_completo, get_soap_remesas, get_soap_partidas, get_soap_acuse, get_soap_edocument
from fastapi.responses import JSONResponse
from core.config import settings
logger = logging.getLogger(__name__)
ESTADO_CREADO = 1
ESTADO_EN_PROCESO = 2
ESTADO_FINALIZADO = 3
ESTADO_ERROR = 4
async def _validate_request_data(request_data: Dict[str, Any]) -> None:
"""
Valida los datos básicos requeridos en las peticiones.
Args:
request_data: Diccionario con datos de la petición
Raises:
HTTPException: Si faltan datos requeridos
"""
if not request_data.get('pedimento'):
logger.error("ID del pedimento no proporcionado en la petición")
raise HTTPException(status_code=400, detail="ID del pedimento es requerido")
if not request_data.get('organizacion'):
logger.error("ID de la organización no proporcionado en la petición")
raise HTTPException(status_code=400, detail="ID de la organización es requerido")
logger.info(f"Validación exitosa - Pedimento: {request_data['pedimento']}, Organización: {request_data['organizacion']}")
async def _get_pedimento_service(pedimento_id: str, service_type: int, operation_name: str) -> Dict[str, Any]:
"""
Obtiene el servicio de pedimento por tipo.
Args:
pedimento_id: ID del pedimento
service_type: Tipo de servicio a obtener
operation_name: Nombre de la operación para logging
Returns:
Dict con datos del servicio
Raises:
HTTPException: Si hay error al obtener el servicio
"""
try:
logger.info(f"Obteniendo servicio tipo {service_type} para pedimento {pedimento_id} - Operación: {operation_name}")
response_service = await rest_controller.get_pedimento_services(pedimento_id, service_type=service_type)
logger.info(response_service)
if not response_service or len(response_service) == 0:
logger.error(f"No se encontró servicio tipo {service_type} para pedimento {pedimento_id}")
raise HTTPException(status_code=404, detail=f"No se encontró servicio de {operation_name}")
logger.info(f"Servicio obtenido exitosamente: {response_service[0].get('id', 'N/A')}")
return response_service[0]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error al obtener servicio de {operation_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"Error al obtener servicio de {operation_name}")
async def _get_vucem_credentials(contribuyente_id: str, operation_name: str) -> Dict[str, Any]:
"""
Obtiene las credenciales VUCEM para un contribuyente.
Args:
contribuyente_id: ID del contribuyente
operation_name: Nombre de la operación para logging
Returns:
Dict con credenciales VUCEM
Raises:
HTTPException: Si hay error al obtener credenciales
"""
try:
logger.info(f"Obteniendo credenciales VUCEM para contribuyente {contribuyente_id} - Operación: {operation_name}")
response_credentials = await rest_controller.get_vucem_credentials(contribuyente_id)
if not response_credentials or len(response_credentials) == 0:
logger.error(f"No se encontraron credenciales VUCEM para contribuyente {contribuyente_id}")
raise HTTPException(status_code=404, detail="Credenciales VUCEM no encontradas")
logger.info("Credenciales VUCEM obtenidas exitosamente")
return response_credentials[0]
except HTTPException:
raise
except Exception as e:
logger.error(f"Error al obtener credenciales VUCEM para {operation_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
raise HTTPException(status_code=500, detail="Error al obtener credenciales VUCEM")
async def _post_edocuments(response_service: dict, identificadores_ed: list):
"""
Helper function para enviar documentos digitalizados a la API.
Args:
response_service: Diccionario con datos del servicio
identificadores_ed: Lista de identificadores ED a enviar
"""
responses = []
for identificador in identificadores_ed:
# Preparar datos del documento
document_data = {
'clave': identificador['clave'],
'descripcion': identificador['descripcion'],
'numero_edocument': identificador['complemento1'],
'organizacion': response_service['organizacion'],
'pedimento': response_service['pedimento']['id']
}
try:
response = await rest_controller.post_edocument(document_data)
if response is None:
logger.warning(f"No se pudo enviar el documento {identificador['complemento1']}")
continue
responses.append(response)
logger.info(f"Documento {identificador['complemento1']} enviado exitosamente")
except Exception as e:
logger.error(f"Error al enviar el documento {identificador['complemento1']}: {e}")
continue
if not responses:
raise HTTPException(status_code=500, detail="No se pudo enviar ningún documento digitalizado")
return responses
async def _post_coves(response_service: dict, coves: list) -> List[Dict[str, Any]]:
responses = []
for cove in coves:
# Preparar datos del documento
document_data = {
'numero_cove': cove,
'organizacion': response_service['organizacion'],
'pedimento': response_service['pedimento']['id']
}
try:
response = await rest_controller.post_cove(document_data)
except Exception as e:
logger.error(f"Error al enviar el numero de cove {cove}: {e}")
continue
if not responses:
raise HTTPException(status_code=500, detail="No se pudo enviar ningún numero de cove")
return responses
async def _update_service_status(service_id: int, estado: int, response_service: dict, operation_name: str = "operación") -> bool:
"""
Actualiza el estado del servicio de manera robusta.
Args:
service_id: ID del servicio
estado: Nuevo estado (1=creado, 2=en proceso, 3=finalizado, 4=error)
response_service: Datos del servicio
operation_name: Nombre de la operación para logging
Returns:
bool: True si se actualizó exitosamente, False en caso contrario
"""
estado_nombres = {
1: "CREADO",
2: "EN_PROCESO",
3: "FINALIZADO",
4: "ERROR"
}
estado_nombre = estado_nombres.get(estado, f"DESCONOCIDO({estado})")
try:
logger.info(f"Actualizando estado del servicio {service_id} a {estado_nombre} - Operación: {operation_name}")
update_data = {
"estado": estado,
"pedimento": response_service['pedimento']['id'],
"organizacion": response_service['organizacion'],
}
logger.info(f"Body enviado al endpoint PUT: {update_data}")
print(f"Body enviado al endpoint PUT: {update_data}")
result = await rest_controller.put_pedimento_service(service_id=service_id, data=update_data)
if result is None:
logger.error(f"Falló la actualización del estado del servicio {service_id} a {estado_nombre}")
return False
logger.info(f"Estado del servicio {service_id} actualizado exitosamente a {estado_nombre}")
return True
except Exception as e:
logger.error(f"Error al actualizar estado del servicio {service_id} a {estado_nombre} - Operación {operation_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return False
async def _create_response(service_data: dict, additional_data: Optional[Dict[str, Any]] = None,
success_message: str = "Operación completada exitosamente") -> Dict[str, Any]:
"""
Crea una respuesta estandarizada para los endpoints.
Args:
service_data: Datos del servicio
additional_data: Datos adicionales a incluir en la respuesta
success_message: Mensaje de éxito personalizado
Returns:
Dict con estructura de respuesta estandarizada
"""
response = {
"success": True,
"message": success_message,
"data": {
"organizacion": service_data['organizacion'],
"servicio": service_data['id'],
"estado": ESTADO_FINALIZADO,
"pedimento_id": service_data['pedimento']['id']
}
}
if additional_data:
response["data"].update(additional_data)
logger.info(f"Respuesta creada exitosamente para servicio {service_data['id']}")
return response
async def _execute_service_safely(service_func, request_data: Dict[str, Any], service_name: str) -> Dict[str, Any]:
"""
Ejecuta un servicio de manera segura capturando errores.
Args:
service_func: Función del servicio a ejecutar
request_data: Datos para la petición
service_name: Nombre del servicio para logging
Returns:
Dict con resultado de la ejecución
"""
try:
logger.info(f"Iniciando ejecución automática de {service_name}...")
# Crear el objeto request apropiado
from schemas.serviceSchema import ServiceRemesaSchema
request_obj = ServiceRemesaSchema(**request_data)
# Ejecutar el servicio
result = await service_func(request_obj)
logger.info(f"Servicio {service_name} ejecutado exitosamente")
return {
"success": True,
"service_name": service_name,
"result": result.body.decode() if hasattr(result, 'body') else str(result),
"status_code": result.status_code if hasattr(result, 'status_code') else 200
}
except Exception as e:
logger.error(f"Error en ejecución automática de {service_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
return {
"success": False,
"service_name": service_name,
"error": str(e),
"status_code": 500
}
async def _execute_service_with_retry(service_func, request_data: Dict[str, Any],
service_name: str, max_retries: int = 2) -> Dict[str, Any]:
"""
Ejecuta un servicio con reintentos automáticos en caso de fallo.
Args:
service_func: Función del servicio a ejecutar
request_data: Datos para la petición
service_name: Nombre del servicio para logging
max_retries: Número máximo de reintentos
Returns:
Dict con resultado de la ejecución
"""
last_error = None
for attempt in range(max_retries + 1):
try:
if attempt > 0:
wait_time = min(2 ** attempt, 30) # Backoff exponencial, máximo 30 segundos
logger.info(f"Reintentando {service_name} en {wait_time} segundos (intento {attempt + 1}/{max_retries + 1})")
await asyncio.sleep(wait_time)
result = await _execute_service_safely(service_func, request_data, service_name)
if result["success"]:
if attempt > 0:
logger.info(f"✅ Servicio {service_name} exitoso en intento {attempt + 1}")
return result
else:
last_error = result.get("error", "Error desconocido")
except Exception as e:
last_error = str(e)
logger.warning(f"Intento {attempt + 1} fallido para {service_name}: {e}")
# Si llegamos aquí, todos los intentos fallaron
logger.error(f"❌ Servicio {service_name} falló después de {max_retries + 1} intentos. Último error: {last_error}")
return {
"success": False,
"service_name": service_name,
"error": f"Falló después de {max_retries + 1} intentos. Último error: {last_error}",
"status_code": 500,
"retries_attempted": max_retries + 1
}
async def _wait_for_service_creation(pedimento_id: str, service_type: int,
timeout: int = 60, check_interval: int = 2) -> bool:
"""
Espera a que un servicio sea creado antes de intentar ejecutarlo.
Args:
pedimento_id: ID del pedimento
service_type: Tipo de servicio a esperar
timeout: Tiempo máximo de espera en segundos
check_interval: Intervalo entre verificaciones en segundos
Returns:
bool: True si el servicio fue encontrado, False si se agotó el timeout
"""
start_time = asyncio.get_event_loop().time()
logger.info(f"Esperando creación de servicio tipo {service_type} para pedimento {pedimento_id} (timeout: {timeout}s)")
attempt = 0
while (asyncio.get_event_loop().time() - start_time) < timeout:
try:
attempt += 1
services = await rest_controller.get_pedimento_services(pedimento_id, service_type=service_type)
if services and len(services) > 0:
logger.info(f"✅ Servicio tipo {service_type} encontrado para pedimento {pedimento_id} (intento {attempt})")
return True
else:
if attempt % 10 == 0: # Log cada 20 segundos aprox
logger.info(f"⏳ Servicio tipo {service_type} aún no encontrado para pedimento {pedimento_id} (intento {attempt}/{timeout//check_interval})")
except Exception as e:
logger.warning(f"Error verificando servicio tipo {service_type}: {e}")
await asyncio.sleep(check_interval)
logger.error(f"❌ Timeout esperando servicio tipo {service_type} para pedimento {pedimento_id} después de {timeout}s")
return False
async def _execute_follow_up_services(pedimento_id: str, organizacion_id: str,
has_remesas: bool = False, has_partidas: bool = False) -> Dict[str, Any]:
"""
Ejecuta automáticamente los servicios de seguimiento después del pedimento completo.
Args:
pedimento_id: ID del pedimento
organizacion_id: ID de la organización
has_remesas: Si el pedimento tiene remesas
has_partidas: Si el pedimento tiene partidas
Returns:
Dict con resultados de la ejecución
"""
logger.info(f"Iniciando ejecución automática de servicios para pedimento {pedimento_id}")
request_data = {
"pedimento": pedimento_id,
"organizacion": organizacion_id
}
# Lista de servicios a ejecutar con sus tipos correspondientes
services_to_execute = []
# Agregar partidas si el pedimento las tiene
if has_partidas:
services_to_execute.append(("partidas", get_partidas, 4))
# Agregar remesas si el pedimento las tiene
if has_remesas:
services_to_execute.append(("remesas", get_remesas, 5))
# Siempre agregar acuses (si existen documentos digitalizados)
services_to_execute.append(("acuse", get_acuse, 6))
# Resultados de ejecución
execution_results = {
"total_services": len(services_to_execute),
"successful_services": 0,
"failed_services": 0,
"results": []
}
# Esperar un poco antes de iniciar para que se completen los servicios creados
logger.info("Esperando a que se completen las creaciones de servicios...")
await asyncio.sleep(10) # Aumentado de 5 a 10 segundos
# Ejecutar servicios secuencialmente para evitar sobrecarga
for service_name, service_func, service_type in services_to_execute:
try:
logger.info(f"🔄 Iniciando procesamiento de {service_name}...")
# Verificar que el servicio exista antes de ejecutar
service_exists = await _wait_for_service_creation(pedimento_id, service_type, timeout=60)
if not service_exists:
execution_results["failed_services"] += 1
execution_results["results"].append({
"success": False,
"service_name": service_name,
"error": f"Servicio tipo {service_type} no encontrado después de esperar",
"status_code": 404
})
logger.warning(f"⚠️ Servicio {service_name} no encontrado, saltando...")
continue
# Ejecutar servicio con reintentos
result = await _execute_service_with_retry(service_func, request_data, service_name, max_retries=2)
execution_results["results"].append(result)
if result["success"]:
execution_results["successful_services"] += 1
logger.info(f"✅ Servicio {service_name} completado exitosamente")
else:
execution_results["failed_services"] += 1
logger.warning(f"❌ Servicio {service_name} falló: {result.get('error', 'Error desconocido')}")
# Esperar entre servicios para no sobrecargar
await asyncio.sleep(3)
except Exception as e:
execution_results["failed_services"] += 1
execution_results["results"].append({
"success": False,
"service_name": service_name,
"error": f"Error crítico: {str(e)}",
"status_code": 500
})
logger.error(f"💥 Error crítico en servicio {service_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
# Log de resumen
success_rate = (execution_results["successful_services"] / execution_results["total_services"]) * 100 if execution_results["total_services"] > 0 else 0
if execution_results["successful_services"] == execution_results["total_services"]:
logger.info(f"🎉 Ejecución automática completada exitosamente - {execution_results['successful_services']}/{execution_results['total_services']} (100%)")
else:
logger.warning(f"⚠️ Ejecución automática completada con errores - Éxito: {execution_results['successful_services']}/{execution_results['total_services']} ({success_rate:.1f}%)")
return execution_results
async def _schedule_follow_up_services(pedimento_id: str, organizacion_id: str,
xml_content: Dict[str, Any]) -> None:
"""
Programa la ejecución de servicios de seguimiento en segundo plano.
Args:
pedimento_id: ID del pedimento
organizacion_id: ID de la organización
xml_content: Contenido XML del pedimento para determinar qué servicios ejecutar
"""
try:
# Determinar qué servicios ejecutar basado en el contenido del pedimento
has_remesas = bool(xml_content.get('remesas', 0))
has_partidas = xml_content.get('numero_partidas', 0) > 0
logger.info(f"Programando servicios automáticos - Remesas: {has_remesas}, Partidas: {has_partidas}")
# Crear tarea en segundo plano
task = asyncio.create_task(
_execute_follow_up_services(
pedimento_id=pedimento_id,
organizacion_id=organizacion_id,
has_remesas=has_remesas,
has_partidas=has_partidas
)
)
# Agregar callback para logging cuando termine
def log_completion(task):
try:
result = task.result()
logger.info(f"Servicios automáticos completados para pedimento {pedimento_id}: {result['successful_services']}/{result['total_services']} exitosos")
except Exception as e:
logger.error(f"Error en servicios automáticos para pedimento {pedimento_id}: {e}")
task.add_done_callback(log_completion)
logger.info(f"Servicios automáticos programados exitosamente para pedimento {pedimento_id}")
except Exception as e:
logger.error(f"Error al programar servicios automáticos: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
logger.error(f"Error inesperado en {operation_name}: {e}")
logger.error(f"Traceback: {traceback.format_exc()}")
# Actualizar estado a error si tenemos service_data
if service_data:
try:
await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
except Exception as update_error:
logger.error(f"Error al actualizar estado del servicio tras fallo: {update_error}")
raise HTTPException(status_code=500, detail=f"Error interno en {operation_name}: {str(e)}")
def _log_operation_summary(operation_name: str, service_id: int, success: bool,
additional_info: Optional[str] = None) -> None:
"""
Registra un resumen de la operación realizada.
Args:
operation_name: Nombre de la operación
service_id: ID del servicio procesado
success: Si la operación fue exitosa
additional_info: Información adicional opcional
"""
status = "EXITOSO" if success else "FALLIDO"
message = f"RESUMEN {operation_name.upper()}: {status} - Servicio ID: {service_id}"
if additional_info:
message += f" - {additional_info}"
if success:
logger.info(message)
else:
logger.error(message)
async def _validate_soap_controller() -> None:
"""
Valida que el controlador SOAP esté disponible.
Raises:
HTTPException: Si el controlador SOAP no está disponible
"""
if not soap_controller:
logger.error("Controlador SOAP no disponible")
raise HTTPException(status_code=500, detail="Servicio SOAP no disponible")