Se agregaron los moduloes de api_v2
This commit is contained in:
1
api/api_v2/modules/remesas/__init__.py
Normal file
1
api/api_v2/modules/remesas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import tasks
|
||||
134
api/api_v2/modules/remesas/controllers.py
Normal file
134
api/api_v2/modules/remesas/controllers.py
Normal 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()
|
||||
|
||||
|
||||
21
api/api_v2/modules/remesas/routers.py
Normal file
21
api/api_v2/modules/remesas/routers.py
Normal 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}
|
||||
15
api/api_v2/modules/remesas/schemas.py
Normal file
15
api/api_v2/modules/remesas/schemas.py
Normal 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
|
||||
|
||||
242
api/api_v2/modules/remesas/services.py
Normal file
242
api/api_v2/modules/remesas/services.py
Normal 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
|
||||
|
||||
|
||||
27
api/api_v2/modules/remesas/tasks.py
Normal file
27
api/api_v2/modules/remesas/tasks.py
Normal 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}
|
||||
|
||||
0
api/api_v2/modules/remesas/tests.py
Normal file
0
api/api_v2/modules/remesas/tests.py
Normal file
Reference in New Issue
Block a user