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(' 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