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 @@
from . import tasks

View File

@@ -0,0 +1,134 @@
from controllers.RESTController import APIRESTController
from controllers.SOAPController import VUCEMController
from typing import List, Dict, Any
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from typing import List, Dict
class RemesaController(APIRESTController):
def __init__(self):
super().__init__()
async def post_cove(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Método para enviar un número de COVE a la API.
Args:
data: Diccionario con los datos del COVE a enviar
"""
return await self._make_request_async('POST', 'customs/coves/', data=data)
class RemesaVUController(VUCEMController):
def __init__(self):
super().__init__() # Implementación específica para Coves VU
def generate_remesas_template(self, username: str, password: str, aduana: str, patente: str, numero_operacion: str, pedimento: str) -> str:
"""
Genera el template SOAP para consultar remesas
Args:
username: Usuario de VUCEM
password: Contraseña de VUCEM
aduana: Código de aduana
patente: Número de patente
Returns:
str: Template SOAP XML completo
"""
soap_template = f'''
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarremesas"
xmlns:com="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes">
<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>
<con:consultarRemesasPeticion>
<con:numeroOperacion>{numero_operacion}</con:numeroOperacion>
<con:peticion>
<com:aduana>{aduana}</com:aduana>
<com:patente>{patente}</com:patente>
<com:pedimento>{pedimento}</com:pedimento>
</con:peticion>
</con:consultarRemesasPeticion>
</soapenv:Body>
</soapenv:Envelope>'''
return soap_template
# Pedimento Completo
class RemesaXMLScraper:
"""
Controlador para scrapear XML de consultar remesas.
Extrae todos los comprobantesVE, junto con remesaAgente y remesaSA.
"""
namespaces = {
"S": "http://schemas.xmlsoap.org/soap/envelope/",
"ns2": "http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta",
"ns3": "http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarremesas",
}
def extract_remesas(self, xml_content: str) -> List[Dict[str, str]]:
"""
Extrae todos los comprobanteVE de un XML de remesas.
Args:
xml_content: Contenido del XML en string.
Returns:
Lista de diccionarios con comprobanteVE, remesaAgente y remesaSA.
"""
try:
root = ET.fromstring(xml_content)
remesas = []
for remesa in root.findall(".//ns3:remesas", self.namespaces):
comprobante = remesa.find("ns3:comprobanteVE", self.namespaces)
agente = remesa.find("ns3:remesaAgente", self.namespaces)
sa = remesa.find("ns3:remesaSA", self.namespaces)
remesas.append({
"comprobanteVE": comprobante.text if comprobante is not None else None,
"remesaAgente": agente.text if agente is not None else None,
"remesaSA": sa.text if sa is not None else None
})
return remesas
except ET.ParseError as e:
print(f"Error al parsear XML: {e}")
return []
except Exception as e:
print(f"Error inesperado: {e}")
return []
def extract_data(self, xml_content: str) -> Dict[str, Any]:
"""
Método de compatibilidad que llama a extract_remesas y devuelve un dict.
Args:
xml_content: Contenido del XML en string.
Returns:
Dict con los datos extraídos del XML.
"""
remesas_data = self.extract_remesas(xml_content)
return {
'coves': remesas_data,
'total_remesas': len(remesas_data)
}
remesa_rest_controller = RemesaController()
remesa_vu_controller = RemesaVUController()
remesa_xml_scraper = RemesaXMLScraper()

View File

@@ -0,0 +1,21 @@
from fastapi import APIRouter, BackgroundTasks, status, HTTPException
from fastapi.responses import JSONResponse
from .schemas import RemesaBaseSchema
from .tasks import process_remesa_request
import logging
logger = logging.getLogger("app.api")
router = APIRouter()
@router.post("/services/remesas/", status_code=status.HTTP_202_ACCEPTED)
async def download_remesa(remesa_request: RemesaBaseSchema):
"""
Endpoint para iniciar la descarga completa de un pedimento.
"""
remesa_dict = remesa_request.model_dump()
# Ejecuta la tarea de Celery de forma asíncrona
task = process_remesa_request.delay(remesa_dict)
# Puedes devolver el ID de la tarea para consultar el estado después
return {"status": "submitted", "detail": "La solicitud de descarga de la remesa ha sido enviada.", "task_id": task.id}

View File

@@ -0,0 +1,15 @@
from typing import Optional, Union, Dict, Any
from uuid import UUID
from datetime import datetime
# CORRECCIÓN CLAVE: Se importa el 'validator' para que el decorador funcione
from pydantic import BaseModel, Field, validator
from schemas.CredencialSchema import CredencialBaseSchema
from api.api_v2.modules.pedimentos.schemas import PedimentoBaseSchema
class RemesaBaseSchema(BaseModel):
pedimento: PedimentoBaseSchema
credencial: CredencialBaseSchema

View File

@@ -0,0 +1,242 @@
"""Servicios para el manejo de pedimentos completos."""
import logging
from typing import Any, Dict, List, Optional
from fastapi import HTTPException
# Importar controladores (nota: el archivo se llama controllers,py con coma)
import sys
import os
sys.path.append(os.path.dirname(__file__))
from .controllers import remesa_rest_controller, remesa_vu_controller, remesa_xml_scraper
from utils.helpers import soap_error
# Logger configurado para el módulo
logger = logging.getLogger("app.api")
async def obtener_remesa(**kwargs) -> Dict[str, Any]:
"""
Consume el servicio web para obtener pedimento completo.
Args:
**kwargs: Debe contener 'credencial' y 'pedimento' con sus respectivos campos
Returns:
Dict con 'documento' y 'xml_content'
Raises:
HTTPException: Si hay errores en la petición o datos faltantes
"""
# Validar datos de entrada
credencial = kwargs.get('credencial', {})
pedimento_data = kwargs.get('pedimento', {})
if not credencial.get('user') or not credencial.get('password'):
raise HTTPException(status_code=400, detail="Credenciales incompletas")
required_fields = ['aduana', 'patente', 'pedimento', 'id', 'organizacion']
missing_fields = [f for f in required_fields if not pedimento_data.get(f)]
if missing_fields:
raise HTTPException(
status_code=400,
detail=f"Datos de pedimento incompletos: {missing_fields}"
)
logger.info(f"Iniciando consulta SOAP para pedimento: {pedimento_data.get('pedimento')}")
try:
# Generar XML SOAP
soap_xml = remesa_vu_controller.generate_remesas_template(
username=credencial.get('user'),
password=credencial.get('password'),
aduana=pedimento_data.get('aduana'),
patente=pedimento_data.get('patente'),
pedimento=pedimento_data.get('pedimento'),
numero_operacion=pedimento_data.get('numero_operacion', '')
)
soap_headers = {
'Content-Type': 'text/xml; charset=utf-8'
}
# Realizar petición SOAP
soap_response = await remesa_vu_controller.make_request_async(
"ventanilla-ws-pedimentos/ConsultarRemesasService?wsdl",
data=soap_xml,
headers=soap_headers
)
if not soap_response:
raise HTTPException(status_code=500, detail="No se recibió respuesta del servicio SOAP")
if soap_error(soap_response):
logger.error(f"Error en respuesta SOAP: {soap_response.text if hasattr(soap_response, 'text') else 'Sin detalles'}")
raise HTTPException(status_code=500, detail="Error en la respuesta del servicio SOAP")
# Extraer datos del XML
try:
remesas_data = remesa_xml_scraper.extract_remesas(soap_response.text)
except Exception as e:
logger.error(f"Error al extraer datos XML: {e}")
raise HTTPException(status_code=500, detail="Error al procesar respuesta XML")
# Generar nombre de archivo
file_name = f"vu_RM_{pedimento_data.get('pedimento_app', 'unknown')}.xml"
# Enviar documento
try:
document_response = await remesa_rest_controller.post_document(
soap_response=soap_response,
organizacion=pedimento_data.get('organizacion'),
pedimento=pedimento_data.get('id'),
file_name=file_name,
document_type=2,
)
except Exception as e:
logger.error(f"Error al enviar documento: {e}")
raise HTTPException(status_code=500, detail="Error al guardar documento")
logger.info(f"Remesa procesada exitosamente: {pedimento_data.get('pedimento')}")
return {
"documento": document_response,
"xml_content": remesas_data
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error inesperado en consume_ws_get_pedimento_completo: {e}")
raise HTTPException(status_code=500, detail=f"Error interno: {str(e)}")
async def post_remesa_data(**kwargs) -> Dict[str, Any]:
"""
Actualiza la información del pedimento en el sistema REST.
Args:
**kwargs: Datos de credencial y pedimento
Returns:
Dict con resultados del procesamiento
Raises:
HTTPException: Si hay errores críticos en el procesamiento
"""
# Inicializar variables de respuesta
result = {
"documento": None,
"coves_procesados": None,
"coves_error": None,
"xml_content": None
}
# Obtener datos del servicio web
try:
ws_data = await obtener_remesa(**kwargs)
result["documento"] = ws_data.get("documento", None)
xml_content = ws_data.get('xml_content', {})
result["xml_content"] = xml_content
if not xml_content:
logger.warning("No se obtuvo contenido XML del servicio web")
return result
except HTTPException:
raise # Re-lanzar HTTPExceptions
except Exception as e:
logger.error(f"Error inesperado al consumir servicio web: {e}")
raise HTTPException(status_code=500, detail=f"Error al obtener datos del pedimento: {str(e)}")
# Procesar COVEs (crítico)
try:
# print(xml_content.get('coves', []))
result["coves_procesados"] = await _process_coves_safely(kwargs, xml_content)
except Exception as e:
logger.warning(f"Error al procesar COVEs: {e}")
result["coves_error"] = str(e)
logger.info("Procesamiento de pedimento completo finalizado")
return result
async def _process_coves_safely(kwargs: Dict[str, Any], xml_content) -> Optional[List[Dict[str, Any]]]:
"""
Procesa los COVEs de manera segura.
"""
coves = xml_content
if not coves:
logger.info("No se encontraron COVEs para procesar")
return None
logger.info(f"Procesando {len(coves)} COVEs encontrados")
result = await _post_coves(kwargs.get('pedimento', {}), coves)
logger.info(f"Se procesaron exitosamente {len(result)} COVEs")
return result
async def _post_coves(pedimento_data: Dict[str, Any], coves: List[Dict[str, str]]) -> List[Dict[str, Any]]:
"""
Envía COVEs al sistema REST.
Args:
pedimento_data: Datos del pedimento
coves: Lista de diccionarios con datos de COVE (comprobanteVE, remesaAgente, remesaSA)
Returns:
Lista de respuestas exitosas
Raises:
HTTPException: Si no se pudo procesar ningún COVE
"""
if not coves:
return []
responses = []
errors = []
for cove in coves:
# Extraer el número de COVE del diccionario
numero_cove = cove.get('comprobanteVE')
if not numero_cove:
logger.warning(f"COVE sin comprobanteVE encontrado: {cove}")
continue
document_data = {
'numero_cove': numero_cove,
'organizacion': pedimento_data.get('organizacion'),
'pedimento': pedimento_data.get('id')
}
try:
response = await remesa_rest_controller.post_cove(document_data)
if response:
responses.append(response)
logger.debug(f"COVE {numero_cove} procesado exitosamente")
except Exception as e:
error_msg = f"Error al procesar COVE {numero_cove}: {str(e)}"
logger.warning(error_msg)
errors.append(error_msg)
if not responses and coves:
error_detail = f"No se pudo procesar ningún COVE. Errores: {'; '.join(errors)}"
logger.error(error_detail)
raise HTTPException(status_code=500, detail=error_detail)
if errors:
logger.warning(f"Se procesaron {len(responses)}/{len(coves)} COVEs. Errores: {len(errors)}")
return responses

View File

@@ -0,0 +1,27 @@
from celery import Celery
from celery_app import celery_app
import asyncio
import logging
from typing import Dict, Any
from contextlib import asynccontextmanager
from .services import post_remesa_data
from api.api_v2.modules.tasks.tasks import run_async_task
@celery_app.task
def process_remesa_request(remesa_request: Dict[str, Any]) -> Dict[str, Any]:
"""
Tarea de Celery para procesar la solicitud de acuse.
Args:
acuse_request: Diccionario con los datos de la solicitud de acuse.
Returns:
Diccionario con la respuesta del acuse.
"""
loop = asyncio.get_event_loop()
remesa_response = loop.run_until_complete(post_remesa_data(**remesa_request))
return {"status": "processed", "data": remesa_response}

View File