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_or_update_document( soap_response=soap_xml, organizacion=organizacion_efc, pedimento=pedimento_id_efc, file_name=file_name_request, document_type=25, identifier=idEdocument_efc, ) 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") pedimento_efc = kwargs.get('pedimento', {}) organizacion_efc = pedimento_efc.get("organizacion", None) pedimento_id_efc = pedimento_efc.get("id", None) pedimento_app = pedimento_efc.get('pedimento_app', 'N/A') idEdocument_efc = kwargs['edoc'].get('numero_edocument', 'N/A') file_name_response = f"vu_AC_{pedimento_app}_{idEdocument_efc}_RESPONSE_ERROR.xml" logger.info(f"Guardando RESPONSE_ERROR: file={file_name_response}, organizacion={organizacion_efc}, pedimento={pedimento_id_efc}") doc_result = await acuse_rest_controller.post_or_update_document( soap_response=response.text, organizacion=organizacion_efc, pedimento=pedimento_id_efc, file_name=file_name_response, document_type=26, identifier=idEdocument_efc, ) if doc_result is None: logger.error(f"post_document retornó None para RESPONSE_ERROR — archivo físico guardado 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", data={"soap_response": response.text[:3000]} ) ) 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_or_update_document( binary_content=pdf_bytes, organizacion=organizacion, pedimento=pedimento_id, file_name=_file_name, document_type=4, identifier=idEdocument_efc, ) 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, "acuse_estado": "descargado" if status else "pendiente", "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) # 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 acuse del EDocument {edoc.get('numero_edocument')} en la API") raise Exception(f"Fallo al actualizar el estatus del acuse del EDocument {edoc.get('numero_edocument')}") return response async def marcar_error_acuse(edoc: dict, pedimento: dict, mensaje: str, definitivo: bool = False): """ Reporta un fallo de descarga del acuse 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["acuse_estado"] = "error" response = await acuse_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 acuse del EDocument {edoc.get('numero_edocument')} en la API") 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