Files

284 lines
9.8 KiB
Python

import base64
import re
import logging
from typing import Any, Dict, Optional
import xml.etree.ElementTree as ET
from fastapi import HTTPException
from .controllers import acuse_vu_controller, acuse_rest_controller
from utils.helpers import soap_error
from ..common import create_service_response, create_error_response
# Logger configurado para el módulo
logger = logging.getLogger("app.api")
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseEdocument',# AcuseCove
'Accept-Encoding': 'gzip,deflate',
}
async def obtener_acuse(**kwargs):
soap_xml = acuse_vu_controller.generate_acuse_template(**kwargs)
# 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)
idEdocument_efc = kwargs['edoc'].get('numero_edocument', 'N/A')
file_name_request = f"vu_AC_{pedimento_app}_{idEdocument_efc}_REQUEST.xml"
document_response = await acuse_rest_controller.post_document(
soap_response=soap_xml,
organizacion=organizacion_efc,
pedimento=pedimento_id_efc,
file_name=file_name_request,
document_type=25, # Tipo de documento para request de acuse VU
)
except Exception as e:
logger.error(f"Error al enviar solicitud SOAP: {e}")
response = await acuse_vu_controller.make_request_async(
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
data=soap_xml,
headers=soap_headers
)
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"]
)
)
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=f"Error en la solicitud SOAP: {response.status_code}",
data={"soap_response": response.text[:500]}
)
)
if soap_error(response):
logger.error("Error SOAP detectado en la respuesta")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error en la respuesta del servicio SOAP",
data={"soap_response": response.text[:500]}
)
)
acuse_base64 = _extract_acuse_data(response.text)
if acuse_base64 is None:
logger.error("No se pudo extraer el acuse del documento")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="No se pudo extraer el acuse del documento",
errors=["El formato de la respuesta SOAP no es el esperado"]
)
)
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=create_error_response(
message="No se pudo decodificar el documento del acuse",
errors=["El contenido Base64 no es válido"]
)
)
# Validar que el PDF sea válido
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={"content_start": str(pdf_bytes[:20])}
)
)
# Mejorar el nombre del archivo usando todos los datos relevantes
pedimento = kwargs.get('pedimento', {})
pedimento_num = pedimento.get('pedimento','')
_file_name = _get_file_name(**kwargs)
# Validar que organización y pedimento no sean None
organizacion = pedimento.get("organizacion", None)
pedimento_id = pedimento.get("id", None)
rest_response = await acuse_rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=organizacion,
pedimento=pedimento_id,
file_name=_file_name,
document_type=4
)
if rest_response is None:
logger.error("Error al enviar el acuse a la API interna")
raise HTTPException(
status_code=500,
detail=create_error_response(
message="Error al guardar el acuse en el sistema",
errors=["No se pudo enviar el documento a la API interna"],
metadata={"file_name": _file_name}
)
)
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}
)
)
acuse_update_response = await change_edocument_status(
edoc=kwargs.get('edoc'),
status=True,
pedimento=pedimento
)
return create_service_response(
message="Acuse procesado exitosamente",
data={
"document_response": rest_response,
"file_name": _file_name,
"pedimento": pedimento_num,
"acuse_update_response": acuse_update_response
},
metadata={
"document_type": 4,
"pedimento_app": pedimento.get('pedimento_app'),
"organizacion": organizacion,
"edoc_number": kwargs.get('edoc', {}).get('numero_edocument'),
"content_type": "application/pdf"
}
)
async def change_edocument_status(edoc: dict, status: bool, pedimento: dict):
data = {
"id": edoc.get("id"),
"acuse_descargado": status,
"numero_edocument": edoc.get("numero_edocument"),
"pedimento": pedimento.get("id"),
"organizacion": pedimento.get("organizacion"),
}
response = await acuse_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
return response
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 
, 
, 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)
# Agregar padding si es necesario
missing_padding = len(cleaned_content) % 4
if missing_padding:
cleaned_content += '=' * (4 - missing_padding)
# Decodificar Base64
decoded_content = base64.b64decode(cleaned_content)
return decoded_content
except Exception as e:
# Intentar con validación estricta deshabilitada
try:
decoded_content = base64.b64decode(cleaned_content, validate=False)
return decoded_content
except Exception as e2:
return None
def _extract_acuse_data(soap_response_text: str) -> dict:
try:
# Primero, extraer la parte XML del contenido multipart
xml_start = soap_response_text.find('<?xml')
if xml_start == -1:
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:
return None
except ET.ParseError as e:
return None
except Exception as e:
return None
def _get_file_name(**kwargs) -> dict:
pedimento = kwargs.get('pedimento', {})
pedimento_app = pedimento.get('pedimento_app', 'N/A')
idEdocument = kwargs['edoc'].get('numero_edocument', 'N/A')
_file_name = f"vu_AC_{pedimento_app}_{idEdocument}.pdf"
return _file_name