Se agregaron los moduloes de api_v2

This commit is contained in:
2025-10-03 23:16:47 -06:00
parent ac075bfeb7
commit 7149515606
60 changed files with 3714 additions and 252 deletions

View File

@@ -0,0 +1,63 @@
# controllers.py
from controllers.RESTController import APIRESTController
from controllers.SOAPController import VUCEMController
from typing import List, Dict, Any
class EdocVuController(VUCEMController):
"""
Controlador para interactuar con el servicio SOAP de la Ventanilla Única
para la descarga de documentos electrónicos (edocs).
"""
def __init__(self):
super().__init__()
def generate_edoc_template(self, **kwargs) -> str:
"""
Genera el template XML de la solicitud SOAP para un edoc.
"""
credencial = kwargs.get("credencial", {})
username = credencial.get("user")
password = credencial.get("password")
# Aquí usamos `numero_documento` en lugar de `idEDocument` para reflejar el esquema de edocs
numero_documento = kwargs['edoc'].get("numero_edocument", "N/A")
soap_template = f'''
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:edoc="http://www.ventanillaunica.gob.mx/consulta/edocs/oxml">
<soapenv:Header>
<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<wsse:UsernameToken>
<wsse:Username>{username}</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">{password}</wsse:Password>
</wsse:UsernameToken>
</wsse:Security>
</soapenv:Header>
<soapenv:Body>
<edoc:consultaEdocumentoPeticion>
<idEdocument>{numero_documento}</idEdocument>
</edoc:consultaEdocumentoPeticion>
</soapenv:Body>
</soapenv:Envelope>
'''
return soap_template
class EdocRESTController(APIRESTController):
"""
Controlador para interactuar con la API REST interna para
guardar documentos electrónicos.
"""
def __init__(self):
super().__init__()
async def put_edocument(self, edocument_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para actualizar un documento digitalizado en la API.
Args:
edocument_id: UUID del documento a actualizar
data: Diccionario con los datos a actualizar
"""
return await self._make_request_async('PUT', f'customs/edocuments/{edocument_id}/', data=data)
# Instancias de los controladores que serán importadas en services.py
edocs_vu_controller = EdocVuController()
edocs_rest_controller = EdocRESTController()

View File

@@ -0,0 +1,41 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import Dict, Any, Optional
from .schemas import EdocumentsSchema, EdocumentsMasivoSchema
from .tasks import process_edoc_download_request, process_edocs_masivo_download_request
from api.api_v2.modules.authentication.services import get_current_user
router = APIRouter()
# --- Nuevas rutas para la descarga de edocs ---
@router.post("/services/download/edoc/", response_model=Dict[str, Any])
async def download_edoc(edoc_request: EdocumentsSchema):
"""
Endpoint para iniciar la descarga de un documento electrónico (edoc).
"""
edoc_dict = edoc_request.model_dump()
# Ejecuta la tarea de Celery de forma asíncrona
task = process_edoc_download_request.delay(edoc_dict)
# Devuelve el ID de la tarea
return {"task_id": task.id, "status": "submitted"}
@router.post("/services/download/all/edocs/", response_model=Dict[str, Any])
async def download_edocs_masivo(edoc_request: EdocumentsMasivoSchema):
"""
Endpoint para iniciar la descarga masiva de documentos electrónicos (edocs).
"""
task_ids = []
edoc_request_dict = edoc_request.model_dump()
# Para cada edoc en la lista, dispara una tarea Celery
for edoc in edoc_request_dict.get('edocs', []):
# Crea un nuevo diccionario de datos para cada tarea
edoc_dict = {
"pedimento": edoc_request_dict.get('pedimento'),
"credencial": edoc_request_dict.get('credencial'),
"edoc": edoc.get('edoc')
}
task = process_edoc_download_request.delay(edoc_dict)
task_ids.append(task.id)
return {"task_ids": task_ids, "status": "submitted", "total": len(task_ids)}

View File

@@ -0,0 +1,22 @@
from pydantic import BaseModel
from schemas.CredencialSchema import CredencialBaseSchema
from api.api_v2.modules.pedimentos.schemas import PedimentoBaseSchema
class EdocumentBaseSchema(BaseModel):
id: int
numero_edocument: str
#Aplica para EDocuments
class EdocumentsSchema(BaseModel):
edoc : EdocumentBaseSchema
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema
class EdocumentsMasivoSchema(BaseModel):
edocs: list[EdocumentBaseSchema]
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema

View File

@@ -0,0 +1,188 @@
from http.client import HTTPException
import base64
import re
import logging
import xml.etree.ElementTree as ET
from utils.helpers import soap_error
from .controllers import edocs_rest_controller, edocs_vu_controller
logger = logging.getLogger("app.api")
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/EdocumentosService/consultarEdocumento',
'Accept-Encoding': 'gzip,deflate',
}
# --- FUNCIONES AUXILIARES ---
def _decode_base64_content(base64_content):
try:
cleaned_content = re.sub(r'&#x[0-9a-fA-F]+;', '', base64_content)
cleaned_content = re.sub(r'&#[0-9]+;', '', cleaned_content)
cleaned_content = re.sub(r'[\s\n\r\t]', '', cleaned_content)
cleaned_content = re.sub(r'[^A-Za-z0-9+/=]', '', cleaned_content)
missing_padding = len(cleaned_content) % 4
if missing_padding:
cleaned_content += '=' * (4 - missing_padding)
return base64.b64decode(cleaned_content)
except Exception as e:
logger.error(f"Error al decodificar Base64: {e}")
try:
return base64.b64decode(cleaned_content, validate=False)
except Exception:
return None
def _extract_edoc_data(soap_response_text: str) -> str:
try:
xml_start = soap_response_text.find('<?xml')
if xml_start == -1:
return None
xml_content = soap_response_text[xml_start:]
boundary_end = xml_content.find('--uuid:')
if boundary_end != -1:
xml_content = xml_content[:boundary_end]
root = ET.fromstring(xml_content.strip())
namespaces = {
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
'ns3': 'http://www.ventanillaunica.gob.mx/ws/consulta/edocs/'
}
edoc_elemento = root.find('.//ns3:responseConsultaEdocumento/documentoBase64', namespaces)
if edoc_elemento is None:
edoc_elemento = root.find('.//documentoBase64')
return edoc_elemento.text.strip() if edoc_elemento is not None and edoc_elemento.text else None
except ET.ParseError as e:
logger.error(f"Error parseando la respuesta SOAP para Edoc: {e}")
return None
except Exception as e:
logger.error(f"Error general en extracción de datos Edoc: {e}")
return None
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):
soap_xml = edocs_vu_controller.generate_edoc_template(**kwargs)
response = await edocs_vu_controller.make_request_async(
"ventanilla-edocs-HA/EdocumentosServiceWS?wsdl",
data=soap_xml,
headers=soap_headers
)
if response is None:
raise Exception("No se obtuvo respuesta del servicio SOAP.")
if response.status_code != 200:
raise Exception(f"Error en la solicitud SOAP: {response.status_code}")
if soap_error(response):
raise Exception("Respuesta SOAP contiene error de VUCEM.")
edoc_base64 = _extract_edoc_data(response.text)
if edoc_base64 is None:
raise Exception("No se pudo extraer el documento de la respuesta SOAP.")
pdf_bytes = _decode_base64_content(edoc_base64)
if not pdf_bytes:
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento")
if not pdf_bytes.startswith(b'%PDF'):
logger.warning("El contenido decodificado no parece ser un PDF válido")
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)
try:
with open(_file_name, "wb") as f:
f.write(pdf_bytes)
logger.info(f"PDF guardado localmente en {_file_name}")
except Exception as e:
logger.error(f"Error guardando el PDF localmente: {e}")
rest_response = await edocs_rest_controller.post_document(
binary_content=pdf_bytes,
organizacion=organizacion,
pedimento=pedimento_id,
file_name=_file_name,
document_type=5
)
if rest_response is None:
raise Exception("No se pudo enviar el documento a la API interna.")
if rest_response.get("id") is None:
raise Exception("La respuesta de la API interna no contiene un ID válido.")
logger.info("Documento enviado, actualizando status de Edoc")
edoc_status_response = await change_edocument_status(
edoc=kwargs.get('edoc'),
status=True,
pedimento=pedimento
)
return {
"document_response": rest_response,
"file_name": _file_name,
"numero_documento": numero_documento,
"edoc_update_response": edoc_status_response if edoc_status_response else None
}
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 edocs_rest_controller.put_edocument(edocument_id=edoc.get("id"), data=data)
return response
async def obtener_edocs_masivo(**kwargs):
logger.info("Iniciando la orquestación de descarga masiva de Edocs.")
numeros_documentos = kwargs.get("edocs", [])
if not numeros_documentos:
return {"status": "warning", "message": "No se encontraron números de documento para procesar."}
for edoc in numeros_documentos:
try:
logger.info(f"Procesando Edoc: {edoc.get('numero_edocument', 'N/A')}")
edoc = {
"edoc": edoc,
"pedimento": kwargs.get("pedimento"),
"credencial": kwargs.get("credencial")
}
await obtener_edoc(**edoc)
logger.info(f"Edoc {edoc.get('numero_edocument', 'N/A')} procesado exitosamente.")
except Exception as e:
logger.error(f"Error procesando Edoc {edoc.get('numero_edocument', 'N/A')}: {str(e)}", exc_info=True)
continue # Continuar con el siguiente edoc en caso de error
return {
"status": "pending",
"total_documentos": len(numeros_documentos),
"message": "La orquestación de descarga masiva ha sido registrada."
}

View File

@@ -0,0 +1,43 @@
from celery_app import celery_app
from .services import obtener_edoc, obtener_edocs_masivo
import asyncio # Necesario para ejecutar funciones async dentro de Celery
@celery_app.task(bind=True)
def process_edoc_download_request(self, edoc_data: dict):
"""
Tarea de Celery para procesar la descarga de un solo documento edoc.
"""
try:
# Ejecutar la función asíncrona dentro del hilo síncrono de Celery
loop = asyncio.get_event_loop()
result = loop.run_until_complete(obtener_edoc(**edoc_data))
return {"status": "success", "result": result}
except Exception as e:
# Manejo de errores
self.update_state(
state='FAILURE',
meta={'exc_type': type(e).__name__, 'exc_message': str(e)}
)
# Es crucial volver a lanzar la excepción para que Celery la marque como fallida
raise e
@celery_app.task(bind=True)
def process_edocs_masivo_download_request(self, edoc_data: dict):
"""
Tarea de Celery para procesar la descarga de múltiples documentos edoc.
Esta tarea orquesta la ejecución, pero puede delegar en el servicio.
"""
try:
# Ejecutar la función asíncrona dentro del hilo síncrono de Celery
loop = asyncio.get_event_loop()
result = loop.run_until_complete(obtener_edocs_masivo(**edoc_data))
return {"status": "success", "result": result}
except Exception as e:
self.update_state(
state='FAILURE',
meta={'exc_type': type(e).__name__, 'exc_message': str(e)}
)
raise