Primera version estable de microservicios
This commit is contained in:
72
.dockerignore
Normal file
72
.dockerignore
Normal file
@@ -0,0 +1,72 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
temp/
|
||||
|
||||
# Documentation
|
||||
docs/
|
||||
*.md
|
||||
README*
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
23
.env.example
Normal file
23
.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# Configuración de la aplicación
|
||||
APP_NAME=EFC Microservice
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=false
|
||||
|
||||
# Configuración del servidor
|
||||
HOST=0.0.0.0
|
||||
PORT=8001
|
||||
API_URL=http://host.docker.internal:8000/api/v1
|
||||
API_TOKEN=1b5b5a41228cbac6d9c373d739f9c36a918e4dd8
|
||||
|
||||
# Configuración de API externa
|
||||
SOAP_SERVICE_URL=https://www.ventanillaunica.gob.mx
|
||||
EXTERNAL_API_TIMEOUT=5
|
||||
MAX_RETRIES=5
|
||||
TIMEOUT=5
|
||||
WAIT_TIME=0
|
||||
VERIFY_SSL=True
|
||||
|
||||
# Configuración de seguridad
|
||||
SECRET_KEY=your-super-secret-key-here
|
||||
ALGORITHM=HS256
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
108
.gitignore
vendored
Normal file
108
.gitignore
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Python template
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
|
||||
.idea/*
|
||||
59
Dockerfile
Normal file
59
Dockerfile
Normal file
@@ -0,0 +1,59 @@
|
||||
# Multi-stage build para optimizar el tamaño de la imagen
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
# Instalar dependencias de compilación
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Crear directorio para las dependencias
|
||||
WORKDIR /app
|
||||
|
||||
# Instalar dependencias en un directorio temporal
|
||||
COPY requirements.txt .
|
||||
RUN pip install --user --no-cache-dir --verbose -r requirements.txt
|
||||
|
||||
# Imagen final
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Establecer variables de entorno para FastAPI
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PATH=/home/fastapi/.local/bin:$PATH
|
||||
|
||||
# Crear usuario no-root para seguridad
|
||||
RUN groupadd -r fastapi && useradd -r -g fastapi fastapi
|
||||
|
||||
# Instalar curl para healthcheck
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Establecer directorio de trabajo
|
||||
WORKDIR /app
|
||||
|
||||
# Copiar dependencias instaladas desde el builder
|
||||
COPY --from=builder /root/.local /home/fastapi/.local
|
||||
|
||||
# Copiar el código de la aplicación
|
||||
COPY . .
|
||||
|
||||
# Crear directorios necesarios y establecer permisos
|
||||
RUN mkdir -p /app/logs /app/uploads /app/temp && \
|
||||
chown -R fastapi:fastapi /app && \
|
||||
chmod -R 755 /app
|
||||
|
||||
# Cambiar al usuario no-root
|
||||
USER fastapi
|
||||
|
||||
# Exponer puerto
|
||||
EXPOSE 8001
|
||||
|
||||
# Healthcheck para verificar que el servicio está funcionando
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8001/api/v1/health || exit 1
|
||||
|
||||
# Comando por defecto con configuración optimizada
|
||||
CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port 8001 --workers 32 --reload"]
|
||||
33
Dockerfile.prod
Normal file
33
Dockerfile.prod
Normal file
@@ -0,0 +1,33 @@
|
||||
# Multi-stage build para optimizar el tamaño de la imagen
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --user --no-cache-dir --verbose -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONPATH=/app
|
||||
ENV PATH=/home/fastapi/.local/bin:$PATH
|
||||
|
||||
RUN groupadd -r fastapi && useradd -r -g fastapi fastapi
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /root/.local /home/fastapi/.local
|
||||
COPY . .
|
||||
|
||||
USER fastapi
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
1
__init__.py
Normal file
1
__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# App package
|
||||
1
api/__init__.py
Normal file
1
api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API package
|
||||
1
api/api_v1/__init__.py
Normal file
1
api/api_v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API v1 package
|
||||
14
api/api_v1/api.py
Normal file
14
api/api_v1/api.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter
|
||||
# En Python, no se pueden usar llaves {} para importar múltiples módulos.
|
||||
# Debes usar paréntesis () para hacer importaciones multilínea.
|
||||
from api.api_v1.endpoints import (
|
||||
health,
|
||||
pedimentos
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Incluir routers de endpoints
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(pedimentos.router, tags=["pedimentos"])
|
||||
|
||||
1
api/api_v1/endpoints/__init__.py
Normal file
1
api/api_v1/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Endpoints package
|
||||
22
api/api_v1/endpoints/health.py
Normal file
22
api/api_v1/endpoints/health.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter
|
||||
from core.config import settings
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Endpoint para verificar el estado del servicio"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
|
||||
@router.get("/")
|
||||
async def root():
|
||||
"""Endpoint raíz del microservicio"""
|
||||
return {
|
||||
"message": f"Bienvenido a {settings.APP_NAME}",
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs"
|
||||
}
|
||||
1352
api/api_v1/endpoints/pedimentos.py
Normal file
1352
api/api_v1/endpoints/pedimentos.py
Normal file
File diff suppressed because it is too large
Load Diff
298
controllers/RESTController.py
Normal file
298
controllers/RESTController.py
Normal file
@@ -0,0 +1,298 @@
|
||||
import requests
|
||||
import asyncio
|
||||
import logging
|
||||
logger = logging.getLogger("app.api")
|
||||
from typing import List, Dict, Any
|
||||
import os
|
||||
import httpx
|
||||
from core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class APIController:
|
||||
"""
|
||||
Controlador para manejar las peticiones a la API.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.API_URL # URL base de la API
|
||||
|
||||
self.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Token {settings.API_TOKEN}' # Token de autenticación
|
||||
}
|
||||
self.timeout = 5 # Timeout para las peticiones a la API
|
||||
|
||||
def _make_request(self, method, endpoint, data=None):
|
||||
"""
|
||||
Método para hacer peticiones a la API.
|
||||
"""
|
||||
|
||||
url = f"{self.base_url}/{endpoint}"
|
||||
try:
|
||||
|
||||
response = requests.request(method, url, json=data, headers=self.headers, timeout=self.timeout)
|
||||
response.raise_for_status() # Lanza un error si la respuesta no es 200
|
||||
result = response.json()
|
||||
return result # Retorna el JSON de la respuesta
|
||||
except requests.RequestException as e:
|
||||
if hasattr(e, 'response') and e.response is not None:
|
||||
print(f"Status code del error: {e.response.status_code}")
|
||||
print(f"Contenido del error: {e.response.text}")
|
||||
return None
|
||||
|
||||
async def get_pedimento_services(self, pedimento, service_type=3) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Método para obtener la lista de servicios desde la API.
|
||||
"""
|
||||
return await self._make_request_async('GET', f'customs/procesamientopedimentos/?pedimento={pedimento}&estado=1&servicio={service_type}')
|
||||
|
||||
async def get_pedimento(self, pedimento_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para obtener un pedimento específico desde la API.
|
||||
|
||||
Args:
|
||||
pedimento: UUID del pedimento a consultar
|
||||
"""
|
||||
return self._make_request('GET', f'customs/pedimentos/{pedimento_id}/')
|
||||
|
||||
async def get_vucem_credentials(self, importador) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para obtener las credenciales de VUCEM desde la API.
|
||||
"""
|
||||
return await self._make_request_async('GET', f'vucem/vucem/?usuario={importador}')
|
||||
|
||||
async def post_pedimento_service(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para crear un nuevo servicio de pedimento en la API.
|
||||
|
||||
Args:
|
||||
data: Diccionario con los datos del servicio a crear
|
||||
"""
|
||||
return await self._make_request_async('POST', 'customs/procesamientopedimentos/', data=data)
|
||||
|
||||
async def put_pedimento_service(self, service_id: int, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para actualizar un servicio de pedimento en la API.
|
||||
"""
|
||||
return await self._make_request_async('PUT', f'customs/procesamientopedimentos/{service_id}/', data=data)
|
||||
|
||||
async def put_pedimento(self, pedimento_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para actualizar un pedimento en la API.
|
||||
"""
|
||||
return await self._make_request_async('PUT', f'customs/pedimentos/{pedimento_id}/', data=data)
|
||||
|
||||
async def post_document(self, soap_response=None, organizacion: str = None, pedimento: str = None, file_name: str = None, document_type: int = 2, binary_content: bytes = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para enviar documentos (XML, PDF, etc.) a la API.
|
||||
|
||||
Args:
|
||||
soap_response: Respuesta del servicio SOAP (para archivos XML)
|
||||
organizacion: UUID de la organización (requerido)
|
||||
pedimento: UUID del pedimento (requerido)
|
||||
file_name: Nombre del archivo con extensión (requerido)
|
||||
document_type: Tipo de documento
|
||||
binary_content: Contenido binario del archivo (para PDFs, etc.)
|
||||
"""
|
||||
import datetime
|
||||
import tempfile
|
||||
import mimetypes
|
||||
|
||||
if not soap_response and not binary_content:
|
||||
print("Error: Debe proporcionar soap_response o binary_content")
|
||||
return None
|
||||
|
||||
if not file_name:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
file_name = f"documento_{timestamp}.bin"
|
||||
|
||||
try:
|
||||
# Extraer extensión del nombre del archivo
|
||||
file_extension = os.path.splitext(file_name)[1].lstrip('.').lower()
|
||||
if not file_extension:
|
||||
file_extension = 'bin' # Extensión por defecto
|
||||
|
||||
# Determinar Content-Type basado en la extensión
|
||||
content_type_map = {
|
||||
'xml': 'application/xml',
|
||||
'pdf': 'application/pdf',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'zip': 'application/zip',
|
||||
'doc': 'application/msword',
|
||||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'xls': 'application/vnd.ms-excel',
|
||||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
}
|
||||
|
||||
content_type = content_type_map.get(file_extension, 'application/octet-stream')
|
||||
|
||||
# Determinar modo de archivo y contenido
|
||||
if binary_content:
|
||||
# Para archivos binarios (PDFs, imágenes, etc.)
|
||||
file_mode = 'wb'
|
||||
temp_suffix = f'.{file_extension}'
|
||||
content = binary_content
|
||||
is_binary = True
|
||||
else:
|
||||
# Para archivos de texto (XML)
|
||||
file_mode = 'w'
|
||||
temp_suffix = f'.{file_extension}'
|
||||
is_binary = False
|
||||
|
||||
# Obtener contenido de la respuesta SOAP
|
||||
if hasattr(soap_response, 'content'):
|
||||
content = soap_response.content.decode('utf-8')
|
||||
elif hasattr(soap_response, 'text'):
|
||||
content = soap_response.text
|
||||
else:
|
||||
content = str(soap_response)
|
||||
|
||||
# Crear archivo temporal con la extensión correcta
|
||||
encoding = None if is_binary else 'utf-8'
|
||||
with tempfile.NamedTemporaryFile(mode=file_mode, suffix=temp_suffix, delete=False, encoding=encoding) as temp_file:
|
||||
temp_file.write(content)
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Preparar headers para multipart/form-data (sin Content-Type)
|
||||
headers = {
|
||||
'Authorization': f'Token {settings.API_TOKEN}'
|
||||
}
|
||||
|
||||
# Calcular tamaño del archivo
|
||||
file_size = os.path.getsize(temp_file_path)
|
||||
|
||||
# Preparar datos del documento
|
||||
document_data = {
|
||||
'organizacion': organizacion,
|
||||
'pedimento': pedimento,
|
||||
'extension': file_extension,
|
||||
'document_type': document_type,
|
||||
'size': file_size
|
||||
}
|
||||
|
||||
# Subir archivo
|
||||
url = f"{self.base_url}/record/documents/"
|
||||
|
||||
# Usar httpx AsyncClient para la petición asíncrona
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
with open(temp_file_path, 'rb') as file:
|
||||
files = {
|
||||
'archivo': (file_name, file.read(), content_type)
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
url,
|
||||
data=document_data, # Datos van como form-data
|
||||
files=files, # Archivo va como multipart
|
||||
headers=headers
|
||||
)
|
||||
|
||||
# Limpiar archivo temporal
|
||||
os.unlink(temp_file_path)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
print(f"Documento {file_extension.upper()} enviado exitosamente: {file_name} (tamaño: {file_size} bytes)")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error al enviar documento: {e}")
|
||||
# Limpiar archivo temporal en caso de error
|
||||
if 'temp_file_path' in locals() and os.path.exists(temp_file_path):
|
||||
os.unlink(temp_file_path)
|
||||
return None
|
||||
|
||||
async def post_edocument(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Método para enviar un documento digitalizado a la API.
|
||||
|
||||
Args:
|
||||
data: Diccionario con los datos del documento a enviar
|
||||
"""
|
||||
return await self._make_request_async('POST', 'customs/edocuments/', data=data)
|
||||
|
||||
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)
|
||||
|
||||
async def get_edocs(self, pedimento: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Método para obtener los documentos digitalizados de un pedimento.
|
||||
|
||||
Args:
|
||||
pedimento: UUID del pedimento a consultar
|
||||
"""
|
||||
return await self._make_request_async('GET', f'customs/edocuments/?pedimento={pedimento}')
|
||||
|
||||
async def get_coves(self, pedimento: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Método para obtener los COVES de un pedimento.
|
||||
|
||||
Args:
|
||||
pedimento: UUID del pedimento a consultar
|
||||
"""
|
||||
return await self._make_request_async('GET', f'customs/coves/?pedimento={pedimento}')
|
||||
|
||||
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)
|
||||
|
||||
async def _make_request_async(self, method: str, endpoint: str, data=None):
|
||||
"""
|
||||
Método asíncrono para hacer peticiones a la API usando httpx.
|
||||
"""
|
||||
|
||||
|
||||
url = f"{self.base_url.rstrip('/')}/{endpoint.lstrip('/')}"
|
||||
logger.warning(f"Realizando petición {method} a {url}")
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
logger.info(f"Haciendo petición {method} a {url}")
|
||||
|
||||
if method.upper() == 'GET':
|
||||
response = await client.get(url, headers=self.headers)
|
||||
elif method.upper() == 'POST':
|
||||
response = await client.post(url, json=data, headers=self.headers)
|
||||
elif method.upper() == 'PUT':
|
||||
response = await client.put(url, json=data, headers=self.headers)
|
||||
elif method.upper() == 'DELETE':
|
||||
response = await client.delete(url, headers=self.headers)
|
||||
else:
|
||||
raise ValueError(f"Método HTTP no soportado: {method}")
|
||||
|
||||
response.raise_for_status()
|
||||
logger.info(f"Respuesta exitosa: {response.status_code}")
|
||||
|
||||
result = response.json() if response.content else {}
|
||||
return result
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
logger.error(f"Timeout en petición a {url}: {e}")
|
||||
return None
|
||||
except httpx.HTTPStatusError as e:
|
||||
logger.error(f"Error HTTP {e.response.status_code} en {url}: {e}")
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en petición a {url}: {e}")
|
||||
import traceback
|
||||
return None
|
||||
|
||||
|
||||
rest_controller = APIController() # Instancia global del controlador REST
|
||||
271
controllers/SOAPController.py
Normal file
271
controllers/SOAPController.py
Normal file
@@ -0,0 +1,271 @@
|
||||
from core.config import settings
|
||||
from dataclasses import dataclass
|
||||
import requests
|
||||
import httpx
|
||||
import datetime
|
||||
import time
|
||||
|
||||
class SOAPController:
|
||||
"""
|
||||
Controlador para manejar las peticiones SOAP.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.base_url = settings.SOAP_SERVICE_URL
|
||||
self.timeout = settings.TIMEOUT # Timeout por default
|
||||
|
||||
async def make_request(self, endpoint, data=None, headers=None, max_retries=5):
|
||||
intento = 0
|
||||
while intento < settings.MAX_RETRIES:
|
||||
try:
|
||||
with httpx.Client(verify=settings.context, timeout=self.timeout) as client:
|
||||
content = data.encode('utf-8') if data else None
|
||||
response = client.post(
|
||||
f"{self.base_url}/{endpoint}",
|
||||
content=content,
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response # ✅ éxito
|
||||
except Exception as e:
|
||||
intento += 1
|
||||
wait_time = 0
|
||||
print(f"[{endpoint}] Error intento {intento}: {e}. Reintentando en {settings.WAIT_TIME}s...")
|
||||
time.sleep(settings.WAIT_TIME)
|
||||
|
||||
print(f"[{endpoint}] Fallo tras {settings.MAX_RETRIES} intentos.")
|
||||
return None
|
||||
|
||||
async def make_request_async(self, endpoint, data=None, headers=None, max_retries=5):
|
||||
"""
|
||||
Método asíncrono para hacer peticiones SOAP sin bloquear el event loop
|
||||
|
||||
Args:
|
||||
endpoint: El endpoint al que se va a hacer la petición
|
||||
data: Los datos a enviar en la petición
|
||||
headers: Los headers HTTP a incluir en la petición
|
||||
max_retries: Número máximo de reintentos en caso de fallo
|
||||
|
||||
Returns:
|
||||
La respuesta de la petición, o None si falla tras los reintentos
|
||||
"""
|
||||
import asyncio
|
||||
intento = 0
|
||||
while intento < settings.MAX_RETRIES:
|
||||
try:
|
||||
async with httpx.AsyncClient(verify=settings.context, timeout=self.timeout) as client:
|
||||
content = data.encode('utf-8') if data else None
|
||||
response = await client.post(
|
||||
f"{self.base_url}/{endpoint}",
|
||||
content=content,
|
||||
headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response # ✅ éxito
|
||||
except Exception as e:
|
||||
intento += 1
|
||||
print(f"[{endpoint}] Error intento {intento}: {e}. Reintentando en {settings.WAIT_TIME}s...")
|
||||
if intento < settings.MAX_RETRIES:
|
||||
await asyncio.sleep(settings.WAIT_TIME) # ASYNC SLEEP!
|
||||
|
||||
print(f"[{endpoint}] Fallo tras {settings.MAX_RETRIES} intentos.")
|
||||
return None
|
||||
|
||||
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
|
||||
|
||||
def generate_pedimento_completo_template(self, username: str, password: str, aduana: str, patente: str, pedimento: str) -> str:
|
||||
"""
|
||||
Genera el template SOAP para consultar pedimento completo
|
||||
|
||||
Args:
|
||||
username: Usuario de VUCEM
|
||||
password: Contraseña de VUCEM
|
||||
aduana: Código de aduana
|
||||
patente: Número de patente
|
||||
pedimento: Número de pedimento
|
||||
|
||||
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/consultarpedimentocompleto"
|
||||
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:consultarPedimentoCompletoPeticion>
|
||||
<con:peticion>
|
||||
<com:aduana>{aduana}</com:aduana>
|
||||
<com:patente>{patente}</com:patente>
|
||||
<com:pedimento>{pedimento}</com:pedimento>
|
||||
</con:peticion>
|
||||
</con:consultarPedimentoCompletoPeticion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>'''
|
||||
|
||||
return soap_template
|
||||
|
||||
def generate_partidas_template(self, username: str, password: str, aduana: str, patente: str, pedimento: str, numero_operacion: str, partida: str) -> str:
|
||||
"""
|
||||
Genera el template SOAP para consultar partidas de un pedimento
|
||||
|
||||
Args:
|
||||
username: Usuario de VUCEM
|
||||
password: Contraseña de VUCEM
|
||||
aduana: Código de aduana
|
||||
patente: Número de patente
|
||||
pedimento: Número de pedimento
|
||||
|
||||
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/consultarpartida" 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:consultarPartidaPeticion>
|
||||
<con:peticion>
|
||||
<com:aduana>{aduana}</com:aduana>
|
||||
<com:patente>{patente}</com:patente>
|
||||
<com:pedimento>{pedimento}</com:pedimento>
|
||||
<con:numeroOperacion>{numero_operacion}</con:numeroOperacion>
|
||||
<con:numeroPartida>{partida}</con:numeroPartida>
|
||||
</con:peticion>
|
||||
</con:consultarPartidaPeticion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
|
||||
return soap_template
|
||||
|
||||
def generate_acuse_template(self, username: str, password: str, idEDocument: str) -> str:
|
||||
soap_template = f'''
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oxml="http://www.ventanillaunica.gob.mx/consulta/acuses/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>
|
||||
<oxml:consultaAcusesPeticion>
|
||||
<idEdocument>{idEDocument}</idEdocument>
|
||||
</oxml:consultaAcusesPeticion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
return soap_template
|
||||
|
||||
def generate_estado_pedimento_template(self, username: str, password: str, aduana: str, patente: str, pedimento: str, numero_operacion: str) -> str:
|
||||
soap_template = f'''
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:con="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarestadopedimentos"
|
||||
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:consultarEstadoPedimentosPeticion>
|
||||
<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:consultarEstadoPedimentosPeticion>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
return soap_template
|
||||
|
||||
def generate_edocument_template(self, username: str, password: str, idEDocument: str) -> str:
|
||||
"""
|
||||
Genera el template SOAP para consultar un EDocument específico
|
||||
|
||||
Args:
|
||||
username: Usuario de VUCEM
|
||||
password: Contraseña de VUCEM
|
||||
idEDocument: ID del EDocument
|
||||
|
||||
Returns:
|
||||
str: Template SOAP XML completo
|
||||
"""
|
||||
soap_template = f'''
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
|
||||
xmlns:tem="http://tempuri.org/">
|
||||
<soapenv:Header>
|
||||
<tem:UserName>{username}</tem:UserName>
|
||||
<tem:Password>{password}</tem:Password>
|
||||
</soapenv:Header>
|
||||
<soapenv:Body>
|
||||
<tem:DocumentoIn>
|
||||
<tem:Edocument>{idEDocument}</tem:Edocument>
|
||||
<tem:IsCertificado>1</tem:IsCertificado>
|
||||
</tem:DocumentoIn>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
'''
|
||||
return soap_template
|
||||
|
||||
soap_controller = SOAPController() # Instancia global del controlador SOAP
|
||||
257
controllers/XMLController.py
Normal file
257
controllers/XMLController.py
Normal file
@@ -0,0 +1,257 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Pedimento Completo
|
||||
@dataclass
|
||||
class XMLScraper: # Clase me extrae datos de Pedimento
|
||||
"""
|
||||
Clase para manejar la extracción de datos de un XML.
|
||||
"""
|
||||
|
||||
def _get_numero_operacion(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el número de operación del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Número de operación como string.
|
||||
"""
|
||||
numero_operacion = root.find('.//ns2:numeroOperacion', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
return numero_operacion.text if numero_operacion is not None else None
|
||||
|
||||
def _get_pedimento(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el pedimento del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Pedimento como string.
|
||||
"""
|
||||
pedimento = root.find('.//ns2:pedimento/ns2:pedimento', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
return pedimento.text if pedimento is not None else None
|
||||
|
||||
def _get_curp_apoderado(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el CURP del apoderado del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
CURP del apoderado como string.
|
||||
"""
|
||||
curp_apoderado = root.find('.//ns2:curpApoderadomandatario', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
return curp_apoderado.text if curp_apoderado is not None else None
|
||||
|
||||
def _get_agente_aduanal(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el RFC del agente aduanal del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
RFC del agente aduanal como string.
|
||||
"""
|
||||
agente_aduanal = root.find('.//ns2:rfcAgenteAduanalSocFactura', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
return agente_aduanal.text if agente_aduanal is not None else None
|
||||
|
||||
def _get_partidas(self, root: ET.Element) -> int:
|
||||
"""
|
||||
Método para obtener el número máximo de partidas del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Número máximo de partidas como entero.
|
||||
"""
|
||||
partidas_elements = root.findall('.//ns2:partidas', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
partidas_values = []
|
||||
for elem in partidas_elements:
|
||||
try:
|
||||
if elem.text is not None:
|
||||
partidas_values.append(int(elem.text))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return max(partidas_values) if partidas_values else None
|
||||
|
||||
def _get_identificadores_ed(self, root: ET.Element) -> list:
|
||||
"""
|
||||
Método para obtener todos los identificadores con clave 'ED' del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Lista de diccionarios con los datos de identificadores ED.
|
||||
"""
|
||||
namespaces = {
|
||||
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
|
||||
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
|
||||
}
|
||||
identificadores_ed = []
|
||||
|
||||
# Buscar todos los elementos identificadores
|
||||
identificadores_elements = root.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
|
||||
|
||||
for identificador in identificadores_elements:
|
||||
try:
|
||||
# Extraer la clave del identificador (está dentro de claveIdentificador con namespace)
|
||||
clave_elem = identificador.find('ns:claveIdentificador/ns:clave', namespaces)
|
||||
clave = clave_elem.text if clave_elem is not None else None
|
||||
|
||||
# Solo procesar si la clave es 'ED'
|
||||
if clave == 'ED':
|
||||
# Extraer descripción (con namespace)
|
||||
descripcion_elem = identificador.find('ns:claveIdentificador/ns:descripcion', namespaces)
|
||||
descripcion = descripcion_elem.text if descripcion_elem is not None else None
|
||||
|
||||
# Extraer complemento1 (con namespace)
|
||||
complemento1_elem = identificador.find('ns:complemento1', namespaces)
|
||||
complemento1 = complemento1_elem.text if complemento1_elem is not None else None
|
||||
|
||||
# Agregar a la lista si tenemos los datos básicos
|
||||
if clave and complemento1:
|
||||
identificadores_ed.append({
|
||||
'clave': clave,
|
||||
'descripcion': descripcion,
|
||||
'complemento1': complemento1
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
# Log del error pero continuar procesando otros identificadores
|
||||
print(f"Error procesando identificador: {e}")
|
||||
continue
|
||||
|
||||
return identificadores_ed
|
||||
|
||||
def _remesas(self, root: ET.Element) -> bool:
|
||||
"""
|
||||
Método para verificar si el pedimento tiene remesas.
|
||||
Busca identificadores con clave 'RC' (REMESAS DE CONSOLIDADO).
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
True si encuentra identificadores con clave 'RC', False en caso contrario.
|
||||
"""
|
||||
namespaces = {
|
||||
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
|
||||
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
|
||||
}
|
||||
|
||||
# Buscar todos los elementos identificadores
|
||||
identificadores_elements = root.findall('.//ns2:identificadores/ns2:identificadores', namespaces)
|
||||
|
||||
for identificador in identificadores_elements:
|
||||
try:
|
||||
# Extraer la clave del identificador
|
||||
clave_elem = identificador.find('ns:claveIdentificador/ns:clave', namespaces)
|
||||
clave = clave_elem.text if clave_elem is not None else None
|
||||
|
||||
# Si encontramos una clave 'RC', el pedimento tiene remesas
|
||||
if clave == 'RC':
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
# Log del error pero continuar procesando otros identificadores
|
||||
print(f"Error procesando identificador para remesas: {e}")
|
||||
continue
|
||||
|
||||
print("No se encontraron remesas (sin identificadores RC)")
|
||||
return False
|
||||
|
||||
def _get_tipo_operacion(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el tipo de operación del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Tipo de operación como string.
|
||||
"""
|
||||
tipo_operacion = root.find('.//ns2:tipoOperacion/ns2:clave', namespaces={'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto'})
|
||||
return tipo_operacion.text if tipo_operacion is not None else None
|
||||
|
||||
def _get_cove(self, root: ET.Element) -> str:
|
||||
"""
|
||||
Método para obtener el número de COVE del XML.
|
||||
|
||||
Args:
|
||||
root: Elemento raíz del XML.
|
||||
|
||||
Returns:
|
||||
Número de COVE como string.
|
||||
"""
|
||||
namespaces = {
|
||||
'ns2': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto',
|
||||
'ns': 'http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes'
|
||||
}
|
||||
facturas = root.findall('.//ns2:facturas', namespaces=namespaces)
|
||||
coves = []
|
||||
for factura in facturas:
|
||||
cove = factura.find('ns2:numero', namespaces)
|
||||
if cove is not None:
|
||||
coves.append(cove.text)
|
||||
else:
|
||||
print("No se encontró <ns2:numero> en la factura.")
|
||||
|
||||
return coves if coves else None
|
||||
|
||||
def extract_data(self, xml_content: str) -> dict:
|
||||
"""
|
||||
Método para extraer datos específicos del XML.
|
||||
|
||||
Args:
|
||||
xml_content: Contenido del XML como string.
|
||||
|
||||
Returns:
|
||||
Diccionario con los datos extraídos.
|
||||
"""
|
||||
try:
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
# Extraer datos con manejo de errores individual
|
||||
data = {}
|
||||
|
||||
data['numero_operacion'] = self._get_numero_operacion(root)
|
||||
data['pedimento'] = self._get_pedimento(root)
|
||||
data['curp_apoderado'] = self._get_curp_apoderado(root)
|
||||
data['agente_aduanal'] = self._get_agente_aduanal(root)
|
||||
data['numero_partidas'] = self._get_partidas(root)
|
||||
data['identificadores_ed'] = self._get_identificadores_ed(root)
|
||||
data['remesas'] = self._remesas(root)
|
||||
data['tipo_operacion'] = self._get_tipo_operacion(root)
|
||||
data['coves'] = self._get_cove(root)
|
||||
|
||||
# Verificar que se extrajeron los datos esenciales
|
||||
if not any([data['numero_operacion'], data['pedimento'], data['curp_apoderado'], data['agente_aduanal'], data['coves']]):
|
||||
return {}
|
||||
|
||||
return data
|
||||
|
||||
except ET.ParseError as e:
|
||||
print(f"Error al parsear el XML: {e}")
|
||||
return {}
|
||||
except Exception as e:
|
||||
print(f"Error inesperado al extraer datos del XML: {e}")
|
||||
return {}
|
||||
|
||||
return extract_xml_data(xml_content)
|
||||
|
||||
|
||||
class XMLControllerRemesas:
|
||||
pass
|
||||
|
||||
class XMLControllerPartidas:
|
||||
pass
|
||||
|
||||
xml_controller = XMLScraper()
|
||||
0
controllers/__init__.py
Normal file
0
controllers/__init__.py
Normal file
1
core/__init__.py
Normal file
1
core/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Core package
|
||||
49
core/config.py
Normal file
49
core/config.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional, ClassVar
|
||||
import ssl
|
||||
import os
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configuración de la aplicación"""
|
||||
APP_NAME: str = "EFC Microservice"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = False
|
||||
|
||||
API_URL: str = "" # Valor por defecto vacío, se carga desde .env
|
||||
API_TOKEN: str = "" # Valor por defecto vacío, se carga desde .env
|
||||
|
||||
# Configuración de API externa
|
||||
SOAP_SERVICE_URL: str = "https://api.ejemplo.com"
|
||||
EXTERNAL_API_TIMEOUT: int = 30
|
||||
|
||||
# SSL context como ClassVar para evitar que sea un field del modelo
|
||||
context: ClassVar[ssl.SSLContext] = ssl.create_default_context()
|
||||
|
||||
# Configuración de reintentos y timeouts
|
||||
MAX_RETRIES: int = 3
|
||||
WAIT_TIME: int = 0
|
||||
VERIFY_SSL: bool = True
|
||||
TIMEOUT: int = 5 # Timeout por defecto para las peticiones HTTP
|
||||
|
||||
# Configuración del servidor
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8001
|
||||
|
||||
# Configuración de seguridad
|
||||
SECRET_KEY: str = "your-secret-key-here"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
model_config = {"env_file": ".env"}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
# Configurar SSL context después de la inicialización
|
||||
if hasattr(self, 'context'):
|
||||
self.context.set_ciphers('DEFAULT:@SECLEVEL=1')
|
||||
|
||||
|
||||
settings = Settings()
|
||||
203
docs/AUTOMATIC_SERVICES_FLOW.md
Normal file
203
docs/AUTOMATIC_SERVICES_FLOW.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Flujo Automático de Servicios de Pedimentos
|
||||
|
||||
## Descripción General
|
||||
|
||||
Después de completar exitosamente el procesamiento de un **pedimento completo**, el sistema ahora ejecuta automáticamente los siguientes servicios en segundo plano:
|
||||
|
||||
1. **Partidas** (si el pedimento tiene partidas)
|
||||
2. **Remesas** (si el pedimento tiene remesas)
|
||||
3. **Acuses** (si existen documentos digitalizados)
|
||||
|
||||
## Flujo de Ejecución
|
||||
|
||||
### 1. Ejecución del Pedimento Completo
|
||||
```
|
||||
POST /services/pedimento_completo
|
||||
```
|
||||
|
||||
El endpoint procesa el pedimento completo y al finalizar exitosamente:
|
||||
- ✅ Crea servicios adicionales automáticamente
|
||||
- ✅ Programa la ejecución automática de servicios de seguimiento
|
||||
- ✅ Retorna respuesta inmediata al cliente
|
||||
|
||||
### 2. Ejecución Automática en Segundo Plano
|
||||
|
||||
El sistema ejecuta automáticamente los siguientes servicios:
|
||||
|
||||
#### Partidas
|
||||
- **Condición**: `numero_partidas > 0`
|
||||
- **Servicio**: `POST /services/partidas`
|
||||
- **Tipo**: 4
|
||||
|
||||
#### Remesas
|
||||
- **Condición**: `remesas = 1` en el XML del pedimento
|
||||
- **Servicio**: `POST /services/remesas`
|
||||
- **Tipo**: 5
|
||||
|
||||
#### Acuses
|
||||
- **Condición**: Siempre se ejecuta (procesará solo si hay documentos digitalizados)
|
||||
- **Servicio**: `POST /services/acuse`
|
||||
- **Tipo**: 6
|
||||
|
||||
## Características del Sistema Automático
|
||||
|
||||
### ⏱️ Timing y Secuencia
|
||||
- **Espera inicial**: 5 segundos después de completar el pedimento completo
|
||||
- **Verificación de servicios**: Espera hasta 30 segundos a que se creen los servicios
|
||||
- **Intervalo entre servicios**: 3 segundos entre cada ejecución
|
||||
- **Ejecución secuencial**: Los servicios se ejecutan uno tras otro, no en paralelo
|
||||
|
||||
### 🔄 Sistema de Reintentos
|
||||
- **Reintentos automáticos**: Hasta 2 reintentos por servicio
|
||||
- **Backoff exponencial**: Tiempo de espera incrementa exponencialmente (2, 4, 8... segundos, máximo 30)
|
||||
- **Tolerancia a fallos**: Si un servicio falla, continúa con los siguientes
|
||||
|
||||
### 📊 Logging y Monitoreo
|
||||
- **Logging detallado**: Cada paso del proceso se registra con emojis para fácil identificación
|
||||
- **Resumen de ejecución**: Al final se muestra un resumen con éxitos/fallos
|
||||
- **Callback de finalización**: Notificación cuando termine todo el proceso
|
||||
|
||||
## Respuesta del Endpoint
|
||||
|
||||
El endpoint `/services/pedimento_completo` ahora retorna información adicional:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Pedimento completo procesado exitosamente. Servicios automáticos programados.",
|
||||
"data": {
|
||||
"organizacion": "uuid-organizacion",
|
||||
"servicio": 123,
|
||||
"estado": 3,
|
||||
"pedimento_id": "uuid-pedimento",
|
||||
"documento": { ... },
|
||||
"xml_content": { ... },
|
||||
"edocuments": [ ... ],
|
||||
"servicios_adicionales": {
|
||||
"servicio_partidas": 124,
|
||||
"servicio_acuse": 125,
|
||||
"servicio_estado_pedimento": 126,
|
||||
"servicio_edocument": 127,
|
||||
"servicio_remesas": 128 // Solo si aplica
|
||||
},
|
||||
"servicios_automaticos": {
|
||||
"programados": true,
|
||||
"remesas_programadas": true,
|
||||
"partidas_programadas": true,
|
||||
"acuses_programados": true,
|
||||
"mensaje": "Los servicios de partidas, remesas y acuses se ejecutarán automáticamente en segundo plano"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Consulta de Estado
|
||||
|
||||
Para verificar el progreso de los servicios automáticos:
|
||||
|
||||
```
|
||||
GET /services/status/{pedimento_id}?organizacion={organizacion_id}
|
||||
```
|
||||
|
||||
### Respuesta de Estado
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"pedimento_id": "uuid-pedimento",
|
||||
"organizacion": "uuid-organizacion",
|
||||
"summary": {
|
||||
"total_services": 6,
|
||||
"completed_services": 4,
|
||||
"in_progress_services": 1,
|
||||
"error_services": 1,
|
||||
"completion_percentage": 66.7
|
||||
},
|
||||
"services": {
|
||||
"pedimento_completo": {
|
||||
"exists": true,
|
||||
"service_id": 123,
|
||||
"estado": 3,
|
||||
"estado_nombre": "FINALIZADO"
|
||||
},
|
||||
"partidas": {
|
||||
"exists": true,
|
||||
"service_id": 124,
|
||||
"estado": 3,
|
||||
"estado_nombre": "FINALIZADO"
|
||||
},
|
||||
"remesas": {
|
||||
"exists": true,
|
||||
"service_id": 128,
|
||||
"estado": 2,
|
||||
"estado_nombre": "EN_PROCESO"
|
||||
},
|
||||
"acuse": {
|
||||
"exists": true,
|
||||
"service_id": 125,
|
||||
"estado": 4,
|
||||
"estado_nombre": "ERROR"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Estados de Servicio
|
||||
|
||||
| Estado | Código | Descripción |
|
||||
|--------|--------|-------------|
|
||||
| CREADO | 1 | Servicio creado, esperando ejecución |
|
||||
| EN_PROCESO | 2 | Servicio ejecutándose actualmente |
|
||||
| FINALIZADO | 3 | Servicio completado exitosamente |
|
||||
| ERROR | 4 | Servicio falló después de reintentos |
|
||||
|
||||
## Logs de Ejemplo
|
||||
|
||||
```
|
||||
2024-07-10 12:00:15 INFO - Pedimento completo procesado exitosamente - Servicio: 123
|
||||
2024-07-10 12:00:16 INFO - Programando servicios automáticos - Remesas: True, Partidas: True
|
||||
2024-07-10 12:00:16 INFO - Servicios automáticos programados exitosamente para pedimento uuid-pedimento
|
||||
2024-07-10 12:00:21 INFO - Esperando a que se completen las creaciones de servicios...
|
||||
2024-07-10 12:00:26 INFO - 🔄 Iniciando procesamiento de partidas...
|
||||
2024-07-10 12:00:26 INFO - Servicio tipo 4 encontrado para pedimento uuid-pedimento
|
||||
2024-07-10 12:00:45 INFO - ✅ Servicio partidas completado exitosamente
|
||||
2024-07-10 12:00:48 INFO - 🔄 Iniciando procesamiento de remesas...
|
||||
2024-07-10 12:01:05 INFO - ✅ Servicio remesas completado exitosamente
|
||||
2024-07-10 12:01:08 INFO - 🔄 Iniciando procesamiento de acuse...
|
||||
2024-07-10 12:01:25 INFO - ✅ Servicio acuse completado exitosamente
|
||||
2024-07-10 12:01:25 INFO - 🎉 Ejecución automática completada exitosamente - 3/3 (100%)
|
||||
2024-07-10 12:01:25 INFO - Servicios automáticos completados para pedimento uuid-pedimento: 3/3 exitosos
|
||||
```
|
||||
|
||||
## Beneficios
|
||||
|
||||
### ✨ Para el Usuario
|
||||
- **Respuesta inmediata**: No espera a que se completen todos los servicios
|
||||
- **Procesamiento automático**: No necesita ejecutar manualmente cada servicio
|
||||
- **Tolerancia a fallos**: Los errores en servicios individuales no afectan el flujo completo
|
||||
|
||||
### 🔧 Para el Sistema
|
||||
- **Desacoplamiento**: El pedimento completo no depende de los servicios secundarios
|
||||
- **Escalabilidad**: Procesamiento en segundo plano no bloquea recursos
|
||||
- **Monitoreo**: Logging detallado para debugging y análisis
|
||||
|
||||
### 📈 Para el Negocio
|
||||
- **Eficiencia**: Automatización reduce tiempo de procesamiento manual
|
||||
- **Confiabilidad**: Sistema de reintentos asegura máxima tasa de éxito
|
||||
- **Visibilidad**: Estado en tiempo real de todos los servicios
|
||||
|
||||
## Consideraciones Técnicas
|
||||
|
||||
### Memoria y Recursos
|
||||
- Las tareas en segundo plano se ejecutan en el mismo proceso
|
||||
- Uso mínimo de memoria adicional
|
||||
- Timeout automático para evitar tareas colgadas
|
||||
|
||||
### Manejo de Errores
|
||||
- Errores en servicios automáticos no afectan la respuesta del pedimento completo
|
||||
- Logs detallados para debugging
|
||||
- Reintentos automáticos con backoff exponencial
|
||||
|
||||
### Concurrencia
|
||||
- Ejecución secuencial evita sobrecarga del sistema VUCEM
|
||||
- Intervalos de espera configurables
|
||||
- Control de recursos mediante timeouts
|
||||
148
docs/REFACTORING_SUMMARY.md
Normal file
148
docs/REFACTORING_SUMMARY.md
Normal file
@@ -0,0 +1,148 @@
|
||||
## Resumen de Refactorización - Endpoints de Pedimentos
|
||||
|
||||
### Endpoints Refactorizados (marcados como #Testeado)
|
||||
|
||||
Los siguientes endpoints han sido completamente refactorizados aplicando buenas prácticas:
|
||||
|
||||
#### 1. `/services/pedimento_completo` ⚡ **CON EJECUCIÓN AUTOMÁTICA**
|
||||
- **Mejoras implementadas:**
|
||||
- Validación robusta de datos de entrada
|
||||
- Manejo de errores específico por operación
|
||||
- Logging detallado con contexto de operación
|
||||
- Actualización de estados de servicio más robusta
|
||||
- Procesamiento mejorado de documentos digitalizados
|
||||
- Creación automática de servicios adicionales con validación
|
||||
- Respuestas estandarizadas con información detallada
|
||||
- Manejo de warnings para errores no críticos
|
||||
- **🆕 EJECUCIÓN AUTOMÁTICA**: Dispara automáticamente partidas, remesas y acuses en segundo plano
|
||||
- **🆕 SISTEMA DE REINTENTOS**: Reintentos automáticos con backoff exponencial
|
||||
- **🆕 TOLERANCIA A FALLOS**: Continúa procesamiento aunque fallen servicios individuales
|
||||
|
||||
#### 2. `/services/partidas`
|
||||
- **Mejoras implementadas:**
|
||||
- Procesamiento individual de cada partida con manejo de errores
|
||||
- Validación de número de partidas antes del procesamiento
|
||||
- Continuidad del proceso aunque fallen partidas individuales
|
||||
- Reporte detallado de partidas exitosas vs fallidas
|
||||
- Logging específico para cada partida procesada
|
||||
|
||||
#### 3. `/services/remesas`
|
||||
- **Mejoras implementadas:**
|
||||
- Simplificación del flujo de procesamiento
|
||||
- Validación mejorada de credenciales y contribuyente
|
||||
- Manejo de errores más específico
|
||||
- Respuesta estandarizada con información del documento generado
|
||||
|
||||
#### 4. `/services/acuse`
|
||||
- **Mejoras implementadas:**
|
||||
- Procesamiento individual de cada documento digitalizado
|
||||
- Validación de documentos antes del procesamiento SOAP
|
||||
- Manejo robusto de documentos sin número de e-document
|
||||
- Continuidad del proceso aunque fallen documentos individuales
|
||||
- Extracción y guardado de PDFs con validación de contenido
|
||||
- Reporte detallado de documentos exitosos vs fallidos
|
||||
|
||||
### Funciones Auxiliares Agregadas
|
||||
|
||||
#### 1. `_validate_request_data()`
|
||||
- Validación centralizada de datos de entrada
|
||||
- Logging detallado de validaciones
|
||||
- Mensajes de error específicos
|
||||
|
||||
#### 2. `_get_pedimento_service()`
|
||||
- Obtención robusta de servicios con manejo de errores
|
||||
- Validación de existencia de servicios
|
||||
- Logging específico por tipo de operación
|
||||
|
||||
#### 3. `_get_vucem_credentials()`
|
||||
- Obtención segura de credenciales VUCEM
|
||||
- Validación de existencia de credenciales
|
||||
- Manejo de errores específico para credenciales
|
||||
|
||||
#### 4. `_update_service_status()` (mejorada)
|
||||
- Actualización robusta de estados con nombres descriptivos
|
||||
- Retorno de éxito/fallo para validación
|
||||
- Logging detallado del proceso de actualización
|
||||
- Manejo de errores mejorado
|
||||
|
||||
#### 5. `_create_response()`
|
||||
- Generación de respuestas estandarizadas
|
||||
- Estructura consistente en todas las respuestas
|
||||
- Información detallada del servicio y estado
|
||||
|
||||
#### 6. `_log_operation_summary()`
|
||||
- Logging de resumen para cada operación
|
||||
- Información consolidada de éxito/fallo
|
||||
- Contexto adicional opcional
|
||||
|
||||
#### 7. `_validate_soap_controller()`
|
||||
- Validación de disponibilidad del controlador SOAP
|
||||
- Prevención de errores por controlador no disponible
|
||||
|
||||
### Buenas Prácticas Implementadas
|
||||
|
||||
#### 1. **Manejo de Errores**
|
||||
- Try-catch específicos por tipo de operación
|
||||
- Propagación controlada de HTTPExceptions
|
||||
- Logging detallado de errores con traceback
|
||||
- Actualización automática de estados en caso de error
|
||||
- Diferenciación entre errores críticos y warnings
|
||||
|
||||
#### 2. **Logging Consistente**
|
||||
- Logging estructurado con contexto de operación
|
||||
- Niveles apropiados (INFO, WARNING, ERROR)
|
||||
- Información de progreso durante procesamiento
|
||||
- Resúmenes de operación al final
|
||||
|
||||
#### 3. **Validación Robusta**
|
||||
- Validación temprana de datos de entrada
|
||||
- Verificación de existencia de recursos
|
||||
- Validación de estados antes de continuar
|
||||
- Manejo de casos edge (documentos sin número, etc.)
|
||||
|
||||
#### 4. **Respuestas Estandarizadas**
|
||||
- Estructura consistente en todas las respuestas
|
||||
- Información detallada de éxito/fallo
|
||||
- Warnings para errores no críticos
|
||||
- Metadata útil (contadores, IDs, etc.)
|
||||
|
||||
#### 5. **Manejo de Estados**
|
||||
- Transiciones de estado explícitas y validadas
|
||||
- Rollback automático en caso de error
|
||||
- Logging de cambios de estado
|
||||
- Validación de actualizaciones exitosas
|
||||
|
||||
#### 6. **Documentación Mejorada**
|
||||
- Docstrings detallados para cada endpoint
|
||||
- Descripción de flujo de procesamiento
|
||||
- Especificación de parámetros y respuestas
|
||||
- Documentación de excepciones posibles
|
||||
|
||||
#### 7. **Typing y Tipado**
|
||||
- Uso de Optional y typing hints
|
||||
- Especificación de tipos de retorno
|
||||
- Mejor IntelliSense y detección de errores
|
||||
|
||||
### Beneficios Obtenidos
|
||||
|
||||
1. **Mantenibilidad**: Código más limpio y organizado
|
||||
2. **Debugging**: Logging detallado facilita la identificación de problemas
|
||||
3. **Robustez**: Mejor manejo de casos edge y errores
|
||||
4. **Consistencia**: Estructura uniforme en todos los endpoints
|
||||
5. **Monitoreo**: Información detallada para monitoring y alertas
|
||||
6. **Escalabilidad**: Funciones auxiliares reutilizables
|
||||
7. **Testing**: Estructura más amigable para pruebas unitarias
|
||||
|
||||
### Archivos Modificados
|
||||
|
||||
- `/api/api_v1/endpoints/pedimentos.py` - Refactorización completa
|
||||
- Imports mejorados con tipado
|
||||
|
||||
### Próximos Pasos Recomendados
|
||||
|
||||
1. **Testing**: Implementar pruebas unitarias para las nuevas funciones
|
||||
2. **Monitoring**: Agregar métricas de performance y contadores
|
||||
3. **Configuración**: Externalizar timeouts y límites a configuración
|
||||
4. **Cache**: Implementar cache para credenciales VUCEM
|
||||
5. **Rate Limiting**: Agregar límites de velocidad para peticiones SOAP
|
||||
6. **Retry Logic**: Implementar reintentos automáticos para peticiones fallidas
|
||||
4
docs/routing_options.md
Normal file
4
docs/routing_options.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Alternativa: Si quieres cambiar el prefijo, puedes usar:
|
||||
# application.include_router(api_router, prefix="") # Sin prefijo
|
||||
# o
|
||||
# application.include_router(api_router) # También sin prefijo
|
||||
0
examples/fire_and_forget_example.py
Normal file
0
examples/fire_and_forget_example.py
Normal file
0
examples/item_service_usage.py
Normal file
0
examples/item_service_usage.py
Normal file
0
examples/soap_controller_usage.py
Normal file
0
examples/soap_controller_usage.py
Normal file
78
main.py
Normal file
78
main.py
Normal file
@@ -0,0 +1,78 @@
|
||||
import logging
|
||||
from fastapi import FastAPI
|
||||
from core.config import settings
|
||||
from api.api_v1.api import api_router
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
# Configuración inicial del logging (debe estar al inicio del archivo)
|
||||
logging.config.dictConfig({
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"default": {
|
||||
"()": "uvicorn.logging.DefaultFormatter",
|
||||
"fmt": "%(levelprefix)s %(asctime)s | %(name)s | %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
"use_colors": True,
|
||||
},
|
||||
"access": {
|
||||
"()": "uvicorn.logging.AccessFormatter",
|
||||
"fmt": '%(levelprefix)s %(asctime)s | %(client_addr)s | "%(request_line)s" %(status_code)s',
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"default": {
|
||||
"formatter": "default",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
"access": {
|
||||
"formatter": "access",
|
||||
"class": "logging.StreamHandler",
|
||||
"stream": "ext://sys.stdout",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"": {"handlers": ["default"], "level": "DEBUG"},
|
||||
"uvicorn.error": {"level": "DEBUG"},
|
||||
"uvicorn.access": {"handlers": ["access"], "level": "DEBUG", "propagate": False},
|
||||
},
|
||||
})
|
||||
|
||||
def create_application() -> FastAPI:
|
||||
"""Función factory para crear la aplicación FastAPI"""
|
||||
application = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
description="EFC Microservice - Un microservicio profesional por AduanaSoft",
|
||||
version=settings.APP_VERSION,
|
||||
debug=settings.DEBUG,
|
||||
)
|
||||
|
||||
# Configuración adicional para loggers específicos
|
||||
app_logger = logging.getLogger("app")
|
||||
app_logger.setLevel(logging.DEBUG if settings.DEBUG else logging.INFO)
|
||||
|
||||
# Incluir el router principal de la API
|
||||
application.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
return application
|
||||
|
||||
app = create_application()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # O especifica ["http://localhost:5173"]
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
log_config=None, # Usamos nuestra configuración
|
||||
log_level="debug" if settings.DEBUG else "info",
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
BIN
output.pdf
Normal file
BIN
output.pdf
Normal file
Binary file not shown.
21
requirements.txt
Normal file
21
requirements.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.9.0
|
||||
certifi==2025.6.15
|
||||
charset-normalizer==3.4.2
|
||||
click==8.2.1
|
||||
fastapi==0.116.0
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httpx==0.28.1
|
||||
idna==3.10
|
||||
pydantic==2.11.7
|
||||
pydantic-settings==2.10.1
|
||||
pydantic_core==2.33.2
|
||||
python-dotenv==1.1.1
|
||||
requests==2.32.4
|
||||
sniffio==1.3.1
|
||||
starlette==0.46.2
|
||||
typing-inspection==0.4.1
|
||||
typing_extensions==4.14.1
|
||||
urllib3==2.5.0
|
||||
uvicorn==0.35.0
|
||||
0
schemas/__init__.py
Normal file
0
schemas/__init__.py
Normal file
8
schemas/acuseSchema.py
Normal file
8
schemas/acuseSchema.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
|
||||
class AcuseSchema(BaseModel):
|
||||
document_id: str
|
||||
|
||||
|
||||
112
schemas/pedimentoSchema.py
Normal file
112
schemas/pedimentoSchema.py
Normal file
@@ -0,0 +1,112 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class PedimentoBaseSchema(BaseModel):
|
||||
"""Esquema base para pedimentos con campos comunes"""
|
||||
aduana: str = Field(..., min_length=3, max_length=3, description="Código de aduana (3 dígitos)", pattern="^[0-9]{3}$")
|
||||
patente: str = Field(..., min_length=4, max_length=4, description="Número de patente (4 dígitos)", pattern="^[0-9]{4}$")
|
||||
pedimento: str = Field(..., min_length=7, max_length=7, description="Número de pedimento (7 dígitos)", pattern="^[0-9]{7}$")
|
||||
|
||||
@field_validator('aduana')
|
||||
def validate_aduana(cls, v):
|
||||
if not v.isdigit():
|
||||
raise ValueError('Aduana debe contener solo dígitos')
|
||||
return v
|
||||
|
||||
@field_validator('patente')
|
||||
def validate_patente(cls, v):
|
||||
if not v.isdigit():
|
||||
raise ValueError('Patente debe contener solo dígitos')
|
||||
return v
|
||||
|
||||
@field_validator('pedimento')
|
||||
def validate_pedimento(cls, v):
|
||||
if not v.isdigit():
|
||||
raise ValueError('Pedimento debe contener solo dígitos')
|
||||
return v
|
||||
|
||||
|
||||
class PedimentoRequest(PedimentoBaseSchema):
|
||||
"""Esquema para solicitudes de pedimento"""
|
||||
pedimento_id: str = Field(..., description="ID único del pedimento")
|
||||
organizacion_id: str = Field(..., description="ID de la organización")
|
||||
|
||||
@field_validator('pedimento_id', 'organizacion_id')
|
||||
def validate_ids(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Los IDs no pueden estar vacíos')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class PedimentoCompletoRequest(PedimentoBaseSchema):
|
||||
"""Esquema para solicitar pedimento completo"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="Usuario para autenticación")
|
||||
password: str = Field(..., min_length=1, description="Contraseña para autenticación")
|
||||
|
||||
@field_validator('username')
|
||||
def validate_username(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Username no puede estar vacío')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class EstadoPedimentoRequest(PedimentoBaseSchema):
|
||||
"""Esquema para consultar estado de pedimento"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="Usuario para autenticación")
|
||||
password: str = Field(..., min_length=1, description="Contraseña para autenticación")
|
||||
numero_operacion: str = Field(..., min_length=1, max_length=20, description="Número de operación del pedimento")
|
||||
|
||||
@field_validator('username')
|
||||
def validate_username(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Username no puede estar vacío')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('numero_operacion')
|
||||
def validate_numero_operacion(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Número de operación no puede estar vacío')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class RemesasRequest(PedimentoBaseSchema):
|
||||
"""Esquema para consultar remesas"""
|
||||
username: str = Field(..., min_length=3, max_length=50, description="Usuario para autenticación")
|
||||
password: str = Field(..., min_length=1, description="Contraseña para autenticación")
|
||||
numero_operacion: str = Field(..., min_length=1, max_length=20, description="Número de operación del pedimento")
|
||||
|
||||
@field_validator('username')
|
||||
def validate_username(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Username no puede estar vacío')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('numero_operacion')
|
||||
def validate_numero_operacion(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Número de operación no puede estar vacío')
|
||||
return v.strip()
|
||||
|
||||
|
||||
class PedimentoResponse(BaseModel):
|
||||
"""Esquema para respuestas de pedimento"""
|
||||
success: bool = Field(..., description="Indica si la operación fue exitosa")
|
||||
message: str = Field(..., description="Mensaje descriptivo de la operación")
|
||||
pedimento_id: Optional[str] = Field(None, description="ID del pedimento procesado")
|
||||
organizacion_id: Optional[str] = Field(None, description="ID de la organización")
|
||||
task_id: Optional[str] = Field(None, description="ID de la tarea en segundo plano")
|
||||
data: Optional[dict] = Field(None, description="Datos adicionales de la respuesta")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"success": True,
|
||||
"message": "Pedimento procesado exitosamente",
|
||||
"pedimento_id": "12345",
|
||||
"organizacion_id": "org-123",
|
||||
"task_id": "task-abc-123",
|
||||
"data": {}
|
||||
}
|
||||
}
|
||||
46
schemas/serviceSchema.py
Normal file
46
schemas/serviceSchema.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ServiceBaseSchema(BaseModel):
|
||||
"""Esquema base para servicios con campos comunes"""
|
||||
estado: int = Field(..., description="ID único del servicio")
|
||||
tipo_procesamiento: int = Field(..., description="ID de la organización")
|
||||
pedimento: str = Field(..., description="ID del estado del servicio")
|
||||
servicio: int = Field(..., description="ID del tipo de servicio")
|
||||
organizacion: str = Field(..., description="ID de la organización")
|
||||
|
||||
|
||||
|
||||
@field_validator('pedimento', 'organizacion')
|
||||
def validate_string_fields(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Los campos de texto no pueden estar vacíos')
|
||||
return v.strip()
|
||||
|
||||
@field_validator('estado', 'tipo_procesamiento', 'servicio')
|
||||
def validate_numeric_ids(cls, v):
|
||||
if v is None or v < 0:
|
||||
raise ValueError('Los IDs numéricos deben ser números positivos')
|
||||
return v
|
||||
|
||||
class ServiceUpdateRequest(ServiceBaseSchema):
|
||||
"""Esquema para actualizar un servicio"""
|
||||
id: int = Field(..., description="ID del servicio a actualizar")
|
||||
|
||||
@field_validator('id')
|
||||
def validate_id(cls, v):
|
||||
if v is None or v < 0:
|
||||
raise ValueError('El ID debe ser un número positivo')
|
||||
return v
|
||||
|
||||
class ServiceRemesaSchema(BaseModel):
|
||||
"""Esquema para remesas de servicios"""
|
||||
organizacion: str = Field(..., description="ID de la organización")
|
||||
pedimento: str = Field(..., description="ID del pedimento")
|
||||
|
||||
@field_validator('organizacion', 'pedimento')
|
||||
def validate_string_fields(cls, v):
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Los campos de texto no pueden estar vacíos')
|
||||
return v.strip()
|
||||
15
schemas/vucemSchema.py
Normal file
15
schemas/vucemSchema.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
|
||||
class VucemSchema(BaseModel):
|
||||
id: str
|
||||
organization_id: str
|
||||
user: str
|
||||
password: str
|
||||
patente: str
|
||||
is_active: bool
|
||||
is_importer: bool
|
||||
acuseCove: bool
|
||||
acuseedocument: bool
|
||||
|
||||
0
services/exceptions.py
Normal file
0
services/exceptions.py
Normal file
439
test.xml
Normal file
439
test.xml
Normal file
@@ -0,0 +1,439 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<S:Header>
|
||||
<wsse:Security
|
||||
xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
|
||||
S:mustUnderstand="1">
|
||||
<wsu:Timestamp
|
||||
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
|
||||
<wsu:Created>2025-07-10T14:24:04Z</wsu:Created>
|
||||
<wsu:Expires>2025-07-10T14:25:04Z</wsu:Expires>
|
||||
</wsu:Timestamp>
|
||||
</wsse:Security>
|
||||
</S:Header>
|
||||
<S:Body>
|
||||
<ns2:consultarPedimentoCompletoRespuesta
|
||||
xmlns="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/comunes"
|
||||
xmlns:ns2="http://www.ventanillaunica.gob.mx/pedimentos/ws/oxml/consultarpedimentocompleto"
|
||||
xmlns:ns3="http://www.ventanillaunica.gob.mx/common/ws/oxml/respuesta"
|
||||
xmlns:ns4="http://www.ventanillaunica.gob.mx/common/ws/oxml/resolucion"
|
||||
xmlns:ns5="http://www.ventanillaunica.gob.mx/common/ws/oxml/respuestatra"
|
||||
xmlns:ns6="http://www.ventanillaunica.gob.mx/common/ws/oxml/dictamen"
|
||||
xmlns:ns7="http://www.ventanillaunica.gob.mx/common/ws/oxml/observacion"
|
||||
xmlns:ns8="http://www.ventanillaunica.gob.mx/common/ws/oxml/requisito"
|
||||
xmlns:ns9="http://www.ventanillaunica.gob.mx/common/ws/oxml/opinion">
|
||||
<ns3:tieneError>false</ns3:tieneError>
|
||||
<ns2:numeroOperacion>21277177344</ns2:numeroOperacion>
|
||||
<ns2:pedimento>
|
||||
<ns2:pedimento>2001238</ns2:pedimento>
|
||||
<ns2:encabezado>
|
||||
<ns2:tipoOperacion>
|
||||
<ns2:clave>1</ns2:clave>
|
||||
<ns2:descripcion>Importacion</ns2:descripcion>
|
||||
</ns2:tipoOperacion>
|
||||
<ns2:claveDocumento>
|
||||
<ns2:clave>IN</ns2:clave>
|
||||
<ns2:descripcion>IMPORTACION TEMPORAL DE INSUMOS POR IMMEX</ns2:descripcion>
|
||||
</ns2:claveDocumento>
|
||||
<ns2:destino>
|
||||
<ns2:clave>9</ns2:clave>
|
||||
<ns2:descripcion>INTERIOR DEL PAIS</ns2:descripcion>
|
||||
</ns2:destino>
|
||||
<ns2:aduanaEntradaSalida>
|
||||
<ns2:clave>230</ns2:clave>
|
||||
<ns2:descripcion>NOGALES, NOGALES, SONORA.</ns2:descripcion>
|
||||
</ns2:aduanaEntradaSalida>
|
||||
<ns2:tipoCambio>20.76130</ns2:tipoCambio>
|
||||
<ns2:pesoBruto>6745.682</ns2:pesoBruto>
|
||||
<ns2:medioTrasnporteSalida>
|
||||
<ns2:clave>7</ns2:clave>
|
||||
<ns2:descripcion>CARRETERO</ns2:descripcion>
|
||||
</ns2:medioTrasnporteSalida>
|
||||
<ns2:medioTrasnporteArribo>
|
||||
<ns2:clave>7</ns2:clave>
|
||||
<ns2:descripcion>CARRETERO</ns2:descripcion>
|
||||
</ns2:medioTrasnporteArribo>
|
||||
<ns2:medioTrasnporteEntrada>
|
||||
<ns2:clave>7</ns2:clave>
|
||||
<ns2:descripcion>CARRETERO</ns2:descripcion>
|
||||
</ns2:medioTrasnporteEntrada>
|
||||
<ns2:curpApoderadomandatario>GUMM710831HSRZRG08</ns2:curpApoderadomandatario>
|
||||
<ns2:rfcAgenteAduanalSocFactura>GLG1502247K9</ns2:rfcAgenteAduanalSocFactura>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
<ns2:valorAduanalTotal>1642523.00</ns2:valorAduanalTotal>
|
||||
<ns2:valorComercialTotal>1642523.00</ns2:valorComercialTotal>
|
||||
</ns2:encabezado>
|
||||
<ns2:rectificacion>
|
||||
<ns2:aduanaOriginal>
|
||||
<ns2:clave>230</ns2:clave>
|
||||
<ns2:descripcion>NOGALES, NOGALES, SONORA.</ns2:descripcion>
|
||||
</ns2:aduanaOriginal>
|
||||
<ns2:aduanaDespacho>
|
||||
<ns2:clave>230</ns2:clave>
|
||||
<ns2:descripcion>NOGALES, NOGALES, SONORA.</ns2:descripcion>
|
||||
</ns2:aduanaDespacho>
|
||||
<ns2:claveDocumento>
|
||||
<ns2:clave>IN</ns2:clave>
|
||||
<ns2:descripcion>IMPORTACION TEMPORAL DE INSUMOS POR IMMEX</ns2:descripcion>
|
||||
</ns2:claveDocumento>
|
||||
<ns2:fechaPago>2022-07-25-06:00</ns2:fechaPago>
|
||||
<ns2:pedimentoOriginal>2009506</ns2:pedimentoOriginal>
|
||||
<ns2:patenteOriginal>1653</ns2:patenteOriginal>
|
||||
<ns2:fechaOriginal>2022-07-15-06:00</ns2:fechaOriginal>
|
||||
</ns2:rectificacion>
|
||||
<ns2:importadorExportador>
|
||||
<ns2:rfc>MTK861014317</ns2:rfc>
|
||||
<ns2:razonSocial>MAQUILAS TETA KAWI S.A. DE C.V. </ns2:razonSocial>
|
||||
<ns2:domicilio>
|
||||
<ns2:calle>CARRETERA INTERNACIONAL GUADALAJARA-NOGALES </ns2:calle>
|
||||
<ns2:numeroExterior>KM 1969</ns2:numeroExterior>
|
||||
<ns2:ciudadMunicipio>Empalme </ns2:ciudadMunicipio>
|
||||
<ns2:codigoPostal>85340 </ns2:codigoPostal>
|
||||
</ns2:domicilio>
|
||||
<ns2:seguros>0.00</ns2:seguros>
|
||||
<ns2:fletes>0.00</ns2:fletes>
|
||||
<ns2:embalajes>0.00</ns2:embalajes>
|
||||
<ns2:incrementables>0.00</ns2:incrementables>
|
||||
<ns2:aaduanaDespacho>
|
||||
<ns2:clave>230</ns2:clave>
|
||||
<ns2:descripcion>NOGALES, NOGALES, SONORA.</ns2:descripcion>
|
||||
</ns2:aaduanaDespacho>
|
||||
<ns2:fechas>
|
||||
<ns2:fecha>2022-07-05-06:00</ns2:fecha>
|
||||
<ns2:tipo>
|
||||
<ns2:clave>1</ns2:clave>
|
||||
<ns2:descripcion>FECHA DE ENTRADA A TERRITORIO NAL.</ns2:descripcion>
|
||||
</ns2:tipo>
|
||||
</ns2:fechas>
|
||||
<ns2:fechas>
|
||||
<ns2:fecha>2022-07-15-06:00</ns2:fecha>
|
||||
<ns2:tipo>
|
||||
<ns2:clave>2</ns2:clave>
|
||||
<ns2:descripcion>FECHA DE PAGO DE LAS CONTRIBUCIONES</ns2:descripcion>
|
||||
</ns2:tipo>
|
||||
</ns2:fechas>
|
||||
<ns2:efectivo>1412.00</ns2:efectivo>
|
||||
<ns2:otros>0</ns2:otros>
|
||||
<ns2:total>1412.00</ns2:total>
|
||||
<ns2:pais>
|
||||
<clave>MEX</clave>
|
||||
<descripcion>MEXICO (ESTADOS UNIDOS MEXICANOS)</descripcion>
|
||||
</ns2:pais>
|
||||
</ns2:importadorExportador>
|
||||
<ns2:tasas>
|
||||
<ns2:contribucion>
|
||||
<ns2:clave>15</ns2:clave>
|
||||
<ns2:descripcion>PREVALIDAAAA</ns2:descripcion>
|
||||
</ns2:contribucion>
|
||||
<ns2:tipoTasa>
|
||||
<clave>2</clave>
|
||||
<descripcion>ESPECIFICO</descripcion>
|
||||
</ns2:tipoTasa>
|
||||
<ns2:tasaAplicable>240.0000000000</ns2:tasaAplicable>
|
||||
<ns2:formaPago>
|
||||
<clave>0</clave>
|
||||
<descripcion>EFECTIVO</descripcion>
|
||||
</ns2:formaPago>
|
||||
<ns2:importe>240.00</ns2:importe>
|
||||
</ns2:tasas>
|
||||
<ns2:tasas>
|
||||
<ns2:contribucion>
|
||||
<ns2:clave>23</ns2:clave>
|
||||
<ns2:descripcion>IVA PREV</ns2:descripcion>
|
||||
</ns2:contribucion>
|
||||
<ns2:tipoTasa>
|
||||
<clave>1</clave>
|
||||
<descripcion>PORCENTUAL</descripcion>
|
||||
</ns2:tipoTasa>
|
||||
<ns2:tasaAplicable>16.0000000000</ns2:tasaAplicable>
|
||||
<ns2:formaPago>
|
||||
<clave>0</clave>
|
||||
<descripcion>EFECTIVO</descripcion>
|
||||
</ns2:formaPago>
|
||||
<ns2:importe>38.00</ns2:importe>
|
||||
</ns2:tasas>
|
||||
<ns2:tasas>
|
||||
<ns2:contribucion>
|
||||
<ns2:clave>1</ns2:clave>
|
||||
<ns2:descripcion>DTA</ns2:descripcion>
|
||||
</ns2:contribucion>
|
||||
<ns2:tipoTasa>
|
||||
<clave>4</clave>
|
||||
<descripcion>ESPECIFICO (CUOTA FIJA) DTA</descripcion>
|
||||
</ns2:tipoTasa>
|
||||
<ns2:tasaAplicable>378.0000000000</ns2:tasaAplicable>
|
||||
<ns2:formaPago>
|
||||
<clave>0</clave>
|
||||
<descripcion>EFECTIVO</descripcion>
|
||||
</ns2:formaPago>
|
||||
<ns2:importe>1134.00</ns2:importe>
|
||||
</ns2:tasas>
|
||||
<ns2:proveedoresCompradores>
|
||||
<ns2:identificadorFiscal>84-401607200</ns2:identificadorFiscal>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
</ns2:proveedoresCompradores>
|
||||
<ns2:proveedoresCompradores>
|
||||
<ns2:identificadorFiscal>84-401607200</ns2:identificadorFiscal>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
</ns2:proveedoresCompradores>
|
||||
<ns2:proveedoresCompradores>
|
||||
<ns2:identificadorFiscal>84-401607200</ns2:identificadorFiscal>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
</ns2:proveedoresCompradores>
|
||||
<ns2:facturas>
|
||||
<ns2:numero>COVE2258M9IT4</ns2:numero>
|
||||
<ns2:terminoFacturacion>
|
||||
<ns2:clave>FCA</ns2:clave>
|
||||
<ns2:descripcion>FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)</ns2:descripcion>
|
||||
</ns2:terminoFacturacion>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:identificadorFiscalProveedorComprador>84-401607200</ns2:identificadorFiscalProveedorComprador>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
</ns2:facturas>
|
||||
<ns2:facturas>
|
||||
<ns2:numero>COVE2257S9033</ns2:numero>
|
||||
<ns2:terminoFacturacion>
|
||||
<ns2:clave>FCA</ns2:clave>
|
||||
<ns2:descripcion>FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)</ns2:descripcion>
|
||||
</ns2:terminoFacturacion>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:identificadorFiscalProveedorComprador>84-401607200</ns2:identificadorFiscalProveedorComprador>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
</ns2:facturas>
|
||||
<ns2:facturas>
|
||||
<ns2:numero>COVE2257PY1Z4</ns2:numero>
|
||||
<ns2:terminoFacturacion>
|
||||
<ns2:clave>FCA</ns2:clave>
|
||||
<ns2:descripcion>FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)</ns2:descripcion>
|
||||
</ns2:terminoFacturacion>
|
||||
<ns2:valorDolares>0.00</ns2:valorDolares>
|
||||
<ns2:valorMonedaExtranjera>0.000000</ns2:valorMonedaExtranjera>
|
||||
<ns2:identificadorFiscalProveedorComprador>84-401607200</ns2:identificadorFiscalProveedorComprador>
|
||||
<ns2:proveedorComprador>LIBRA GUAYMAS LLC</ns2:proveedorComprador>
|
||||
</ns2:facturas>
|
||||
<ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>PC</clave>
|
||||
<descripcion>PEDIMENTO CONSOLIDADO</descripcion>
|
||||
</claveIdentificador>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0170220NCKKN2</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0433220889CP2</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0436220ER86M4</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>SO</clave>
|
||||
<descripcion>SOCIO COMERCIAL CERTIFICADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>AA</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0436220ER86H4</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>CI</clave>
|
||||
<descripcion>CERTIFICACION EN MATERIA DE IVA E IEPS</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>AAA</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>IM</clave>
|
||||
<descripcion>AUTORIZACION DE EMPRESA CON PROGRAMA IMMEX</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>45242006</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>PP</clave>
|
||||
<descripcion>PROGRAMAS DE PROMOCIÓN SECTORIAL.</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>20011635</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0170220NG6SJ4</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>RC</clave>
|
||||
<descripcion>REMESAS DE CONSOLIDADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>1-3</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>ED</clave>
|
||||
<descripcion>E_DOCUMENT DOCUMENTO DIGITALIZADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>0436220ESLMS1</complemento1>
|
||||
</ns2:identificadores>
|
||||
<ns2:identificadores>
|
||||
<claveIdentificador>
|
||||
<clave>IC</clave>
|
||||
<descripcion>IMPORTADOR CERTIFICADO</descripcion>
|
||||
</claveIdentificador>
|
||||
<complemento1>O</complemento1>
|
||||
</ns2:identificadores>
|
||||
</ns2:identificadores>
|
||||
<ns2:observaciones>PEDIMENTO CONSOLIDADO DE IMPORTACION DE CONFORMIDAD CON LOS
|
||||
ARTICULOS 37, 37-A DE LA LEY ADUANERA Y REGLA DE COMERCIO EXTERIOR 1.9.19.,
|
||||
CORRESPONDIENTE A LA SEMANA DEL 04 DE JULIO AL 10 DE JULIO DEL 2022. DE
|
||||
CONFORMIDAD CON EL ARTICULO 89DE LA LEY ADUANERA SE REALIZA RECTIFICACION DE
|
||||
PEDIMENTO PARA MODIFICAR LO SIGUIENTE: SE RECTIFICA PEDIMENTO EN: VALOR
|
||||
COMERCIAL DICE: 73,817.52 DEBE DECIR: 79,114.63 PESO BRUTO DICE: 6695.390 DEBE
|
||||
DECIR: 6745.682 SE RECTIFICA COVE DE FACTURA LIBRA7902RM DICE: COVE2257X6DZ1
|
||||
DEBE DECIR: COVE2258M9IT4 SE CORRIGE PARTIDA # 81 EN CANTIDAD Y UNIDAD DE MEDIDA
|
||||
SE AGREGAN PARTIDAS # 93, 94, 95 y 96. SE DIGITALIZA FACTURA E DOCUMENT
|
||||
0170220NG6SJ4 SE DIGITALIZA REGLA 8va E DOCUMENT 0436220ESLMS1 - - Relacion de
|
||||
facturas - - FACTR: LIBRA GUAYMAS
|
||||
LLC,84-401607200,LIBRA7896RM,05-07-2022,COVE2257PY1Z4,USD,26,664.250 FACTR:
|
||||
LIBRA GUAYMAS
|
||||
LLC,84-401607200,LIBRA7898RM,06-07-2022,COVE2257S9033,USD,5,422.700 FACTR: LIBRA
|
||||
GUAYMAS LLC,84-401607200,LIBRA7902RM,08-07-2022,COVE2258M9IT4,USD,47,027.680 </ns2:observaciones>
|
||||
<ns2:partidas>96</ns2:partidas>
|
||||
<ns2:partidas>56</ns2:partidas>
|
||||
<ns2:partidas>7</ns2:partidas>
|
||||
<ns2:partidas>69</ns2:partidas>
|
||||
<ns2:partidas>43</ns2:partidas>
|
||||
<ns2:partidas>95</ns2:partidas>
|
||||
<ns2:partidas>64</ns2:partidas>
|
||||
<ns2:partidas>70</ns2:partidas>
|
||||
<ns2:partidas>8</ns2:partidas>
|
||||
<ns2:partidas>65</ns2:partidas>
|
||||
<ns2:partidas>16</ns2:partidas>
|
||||
<ns2:partidas>79</ns2:partidas>
|
||||
<ns2:partidas>20</ns2:partidas>
|
||||
<ns2:partidas>48</ns2:partidas>
|
||||
<ns2:partidas>26</ns2:partidas>
|
||||
<ns2:partidas>18</ns2:partidas>
|
||||
<ns2:partidas>27</ns2:partidas>
|
||||
<ns2:partidas>54</ns2:partidas>
|
||||
<ns2:partidas>34</ns2:partidas>
|
||||
<ns2:partidas>93</ns2:partidas>
|
||||
<ns2:partidas>44</ns2:partidas>
|
||||
<ns2:partidas>17</ns2:partidas>
|
||||
<ns2:partidas>90</ns2:partidas>
|
||||
<ns2:partidas>37</ns2:partidas>
|
||||
<ns2:partidas>9</ns2:partidas>
|
||||
<ns2:partidas>11</ns2:partidas>
|
||||
<ns2:partidas>51</ns2:partidas>
|
||||
<ns2:partidas>73</ns2:partidas>
|
||||
<ns2:partidas>36</ns2:partidas>
|
||||
<ns2:partidas>66</ns2:partidas>
|
||||
<ns2:partidas>63</ns2:partidas>
|
||||
<ns2:partidas>40</ns2:partidas>
|
||||
<ns2:partidas>88</ns2:partidas>
|
||||
<ns2:partidas>19</ns2:partidas>
|
||||
<ns2:partidas>59</ns2:partidas>
|
||||
<ns2:partidas>15</ns2:partidas>
|
||||
<ns2:partidas>2</ns2:partidas>
|
||||
<ns2:partidas>94</ns2:partidas>
|
||||
<ns2:partidas>68</ns2:partidas>
|
||||
<ns2:partidas>78</ns2:partidas>
|
||||
<ns2:partidas>32</ns2:partidas>
|
||||
<ns2:partidas>39</ns2:partidas>
|
||||
<ns2:partidas>89</ns2:partidas>
|
||||
<ns2:partidas>62</ns2:partidas>
|
||||
<ns2:partidas>4</ns2:partidas>
|
||||
<ns2:partidas>47</ns2:partidas>
|
||||
<ns2:partidas>74</ns2:partidas>
|
||||
<ns2:partidas>41</ns2:partidas>
|
||||
<ns2:partidas>80</ns2:partidas>
|
||||
<ns2:partidas>6</ns2:partidas>
|
||||
<ns2:partidas>42</ns2:partidas>
|
||||
<ns2:partidas>28</ns2:partidas>
|
||||
<ns2:partidas>92</ns2:partidas>
|
||||
<ns2:partidas>49</ns2:partidas>
|
||||
<ns2:partidas>12</ns2:partidas>
|
||||
<ns2:partidas>13</ns2:partidas>
|
||||
<ns2:partidas>84</ns2:partidas>
|
||||
<ns2:partidas>10</ns2:partidas>
|
||||
<ns2:partidas>31</ns2:partidas>
|
||||
<ns2:partidas>87</ns2:partidas>
|
||||
<ns2:partidas>50</ns2:partidas>
|
||||
<ns2:partidas>76</ns2:partidas>
|
||||
<ns2:partidas>22</ns2:partidas>
|
||||
<ns2:partidas>30</ns2:partidas>
|
||||
<ns2:partidas>53</ns2:partidas>
|
||||
<ns2:partidas>71</ns2:partidas>
|
||||
<ns2:partidas>38</ns2:partidas>
|
||||
<ns2:partidas>3</ns2:partidas>
|
||||
<ns2:partidas>33</ns2:partidas>
|
||||
<ns2:partidas>45</ns2:partidas>
|
||||
<ns2:partidas>23</ns2:partidas>
|
||||
<ns2:partidas>25</ns2:partidas>
|
||||
<ns2:partidas>21</ns2:partidas>
|
||||
<ns2:partidas>67</ns2:partidas>
|
||||
<ns2:partidas>85</ns2:partidas>
|
||||
<ns2:partidas>14</ns2:partidas>
|
||||
<ns2:partidas>46</ns2:partidas>
|
||||
<ns2:partidas>29</ns2:partidas>
|
||||
<ns2:partidas>35</ns2:partidas>
|
||||
<ns2:partidas>91</ns2:partidas>
|
||||
<ns2:partidas>24</ns2:partidas>
|
||||
<ns2:partidas>72</ns2:partidas>
|
||||
<ns2:partidas>82</ns2:partidas>
|
||||
<ns2:partidas>61</ns2:partidas>
|
||||
<ns2:partidas>86</ns2:partidas>
|
||||
<ns2:partidas>83</ns2:partidas>
|
||||
<ns2:partidas>77</ns2:partidas>
|
||||
<ns2:partidas>75</ns2:partidas>
|
||||
<ns2:partidas>58</ns2:partidas>
|
||||
<ns2:partidas>81</ns2:partidas>
|
||||
<ns2:partidas>55</ns2:partidas>
|
||||
<ns2:partidas>57</ns2:partidas>
|
||||
<ns2:partidas>5</ns2:partidas>
|
||||
<ns2:partidas>52</ns2:partidas>
|
||||
<ns2:partidas>60</ns2:partidas>
|
||||
<ns2:partidas>1</ns2:partidas>
|
||||
<ns2:diferenciasContribuciones>
|
||||
<ns2:claveGravamen>
|
||||
<ns2:descripcion>DTA</ns2:descripcion>
|
||||
<ns2:clave>1</ns2:clave>
|
||||
</ns2:claveGravamen>
|
||||
<ns2:importePago>364.00</ns2:importePago>
|
||||
<ns2:formaPago>
|
||||
<clave>0</clave>
|
||||
<descripcion>EFECTIVO</descripcion>
|
||||
</ns2:formaPago>
|
||||
</ns2:diferenciasContribuciones>
|
||||
</ns2:pedimento>
|
||||
</ns2:consultarPedimentoCompletoRespuesta>
|
||||
</S:Body>
|
||||
</S:Envelope>
|
||||
0
utils/__init__.py
Normal file
0
utils/__init__.py
Normal file
884
utils/peticiones.py
Normal file
884
utils/peticiones.py
Normal file
@@ -0,0 +1,884 @@
|
||||
import logging
|
||||
logger = logging.getLogger("app.api")
|
||||
from fastapi import HTTPException
|
||||
from controllers.RESTController import rest_controller
|
||||
from typing import Dict, Any
|
||||
import xml.etree.ElementTree as ET
|
||||
import base64
|
||||
import re
|
||||
|
||||
from schemas.serviceSchema import ServiceBaseSchema
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from controllers.XMLController import xml_controller
|
||||
|
||||
|
||||
def validate_pedimento_data(response_service: Dict[str, Any], credenciales: Dict[str, Any]) -> tuple: # Testeado
|
||||
"""
|
||||
Valida y extrae los datos necesarios para la petición SOAP.
|
||||
|
||||
Args:
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
credenciales: Credenciales VUCEM
|
||||
|
||||
Returns:
|
||||
tuple: (username, password, aduana, patente, pedimento)
|
||||
|
||||
Raises:
|
||||
HTTPException: Si faltan datos requeridos
|
||||
"""
|
||||
# Validar credenciales
|
||||
username = credenciales.get('usuario')
|
||||
password = credenciales.get('password')
|
||||
|
||||
if not username or not password:
|
||||
logger.error("Credenciales VUCEM incompletas")
|
||||
raise HTTPException(status_code=400, detail="Credenciales VUCEM incompletas")
|
||||
|
||||
# Validar datos del pedimento
|
||||
pedimento_data = response_service.get('pedimento', {})
|
||||
aduana = pedimento_data.get('aduana')
|
||||
patente = pedimento_data.get('patente')
|
||||
pedimento = pedimento_data.get('pedimento')
|
||||
numero_operacion = pedimento_data.get('numero_operacion')
|
||||
|
||||
if not all([aduana, patente, pedimento]):
|
||||
logger.error(f"Datos del pedimento incompletos - Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
|
||||
raise HTTPException(status_code=400, detail="Datos del pedimento incompletos")
|
||||
|
||||
return username, password, aduana, patente, pedimento, numero_operacion
|
||||
|
||||
def extract_acuse_documento_from_soap(soap_response_text): # Testeado
|
||||
"""
|
||||
Extrae el contenido del tag <acuseDocumento> de la respuesta SOAP multipart.
|
||||
|
||||
Args:
|
||||
soap_response_text (str): Contenido de la respuesta SOAP
|
||||
|
||||
Returns:
|
||||
str: Contenido Base64 del acuseDocumento o None si no se encuentra
|
||||
"""
|
||||
try:
|
||||
# Primero, extraer la parte XML del contenido multipart
|
||||
xml_start = soap_response_text.find('<?xml')
|
||||
if xml_start == -1:
|
||||
logger.error("No se encontró contenido XML en la respuesta SOAP")
|
||||
return None
|
||||
|
||||
# Extraer solo la parte XML
|
||||
xml_content = soap_response_text[xml_start:]
|
||||
|
||||
# Si hay más contenido multipart después, cortarlo
|
||||
boundary_end = xml_content.find('--uuid:')
|
||||
if boundary_end != -1:
|
||||
xml_content = xml_content[:boundary_end]
|
||||
|
||||
# Parsear el XML
|
||||
root = ET.fromstring(xml_content.strip())
|
||||
|
||||
# Buscar el elemento acuseDocumento con namespaces
|
||||
namespaces = {
|
||||
'S': 'http://schemas.xmlsoap.org/soap/envelope/',
|
||||
'ns3': 'http://www.ventanillaunica.gob.mx/ws/consulta/acuses/'
|
||||
}
|
||||
|
||||
# Buscar el elemento acuseDocumento
|
||||
acuse_elemento = root.find('.//ns3:responseConsultaAcuses/acuseDocumento', namespaces)
|
||||
|
||||
if acuse_elemento is None:
|
||||
# Intentar sin namespace
|
||||
acuse_elemento = root.find('.//acuseDocumento')
|
||||
|
||||
if acuse_elemento is not None and acuse_elemento.text:
|
||||
return acuse_elemento.text.strip()
|
||||
else:
|
||||
logger.error("No se encontró el tag <acuseDocumento> o está vacío")
|
||||
return None
|
||||
|
||||
except ET.ParseError as e:
|
||||
logger.error(f"Error parseando XML: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error extrayendo acuseDocumento: {e}")
|
||||
return None
|
||||
|
||||
def extract_pdf_bytes_from_xml(xml_path):
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
# Busca el tag <File> (ajusta el namespace si es necesario)
|
||||
file_elem = root.find('.//File')
|
||||
if file_elem is not None and file_elem.text:
|
||||
# Limpia el contenido base64
|
||||
base64_data = file_elem.text.strip().replace('\n', '').replace('\r', '')
|
||||
pdf_bytes = base64.b64decode(base64_data)
|
||||
cadena_original = None
|
||||
sello_digital = None
|
||||
|
||||
# Buscar CadenaOriginal y SelloDigital en el XML
|
||||
cadena_elem = root.find('.//CadenaOriginal')
|
||||
if cadena_elem is not None and cadena_elem.text:
|
||||
cadena_original = cadena_elem.text.strip()
|
||||
|
||||
sello_elem = root.find('.//SelloDigital')
|
||||
if sello_elem is not None and sello_elem.text:
|
||||
sello_digital = sello_elem.text.strip()
|
||||
|
||||
return {
|
||||
"pdf_bytes": pdf_bytes,
|
||||
"cadena_original": cadena_original,
|
||||
"sello_digital": sello_digital
|
||||
}
|
||||
return pdf_bytes
|
||||
|
||||
else:
|
||||
raise ValueError("No se encontró el tag <File> con contenido válido.")
|
||||
|
||||
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)
|
||||
|
||||
logger.info(f"Contenido Base64 limpiado: {len(cleaned_content)} caracteres")
|
||||
|
||||
# Agregar padding si es necesario
|
||||
missing_padding = len(cleaned_content) % 4
|
||||
if missing_padding:
|
||||
cleaned_content += '=' * (4 - missing_padding)
|
||||
logger.info(f"Padding agregado: {4 - missing_padding} caracteres '='")
|
||||
|
||||
# Decodificar Base64
|
||||
decoded_content = base64.b64decode(cleaned_content)
|
||||
|
||||
logger.info(f"Contenido decodificado exitosamente: {len(decoded_content)} bytes")
|
||||
return decoded_content
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error decodificando Base64: {e}")
|
||||
|
||||
# Intentar con validación estricta deshabilitada
|
||||
try:
|
||||
logger.info("Intentando decodificación con validación relajada...")
|
||||
decoded_content = base64.b64decode(cleaned_content, validate=False)
|
||||
logger.info("¡Decodificación exitosa con validación relajada!")
|
||||
return decoded_content
|
||||
except Exception as e2:
|
||||
logger.error(f"Error también con validación relajada: {e2}")
|
||||
return None
|
||||
|
||||
def soap_error(soap_response): # Testeado
|
||||
"""
|
||||
Verifica si la respuesta SOAP no contiene errores.
|
||||
|
||||
Args:
|
||||
soap_response: Respuesta del servicio SOAP
|
||||
|
||||
Returns:
|
||||
bool: True si no hay errores, False en caso contrario
|
||||
"""
|
||||
if '<ns3:tieneError>true</ns3:tieneError>' in soap_response.text:
|
||||
return True
|
||||
|
||||
# Aquí podrías agregar más lógica para verificar errores específicos en el XML
|
||||
return False
|
||||
|
||||
async def get_soap_pedimento_completo(credenciales, response_service, soap_controller): # Testeado
|
||||
"""
|
||||
Procesa la petición SOAP para obtener el pedimento completo y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, _ = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_pedimento_completo_template(
|
||||
username=username,
|
||||
password=password,
|
||||
aduana=aduana,
|
||||
patente=patente,
|
||||
pedimento=pedimento
|
||||
)
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8'
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla-ws-pedimentos/ConsultarPedimentoCompletoService?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
data = xml_controller.extract_data(soap_response.text)
|
||||
remesas = 1 if data.get('remesas', 0) else 2
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = data.get('numero_partidas', 0)
|
||||
tipo_operacion = data.get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
|
||||
_file_name = f"vu_PC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
|
||||
# Enviar el documento XML como respuesta
|
||||
document_response = await rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=2
|
||||
)
|
||||
|
||||
data['organizacion'] = response_service['organizacion']
|
||||
data['id'] = response_service['pedimento']['id']
|
||||
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response,
|
||||
"xml_content": data
|
||||
}
|
||||
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_pedimento_completo: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar pedimento completo: {str(e)}")
|
||||
|
||||
async def get_soap_remesas(credenciales, response_service, soap_controller): # Testeado
|
||||
"""
|
||||
Procesa la petición SOAP para obtener remesas y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_remesas_template(
|
||||
username=username,
|
||||
password=password,
|
||||
aduana=aduana,
|
||||
patente=patente,
|
||||
pedimento=pedimento,
|
||||
numero_operacion=numero_operacion
|
||||
)
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8'
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla-ws-pedimentos/ConsultarRemesasService?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
# data = xml_controller.extract_data(soap_response.text)
|
||||
# # Enviar el documento XML como respuesta
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
|
||||
_file_name = f"vu_RM_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
|
||||
|
||||
document_response = await rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=3
|
||||
)
|
||||
|
||||
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_remesas: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar remesas: {str(e)}")
|
||||
|
||||
async def get_soap_partidas(credenciales, response_service, soap_controller, partida): # Testeado
|
||||
"""
|
||||
Procesa la petición SOAP para obtener partidas de un pedimento y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
partida: Número de partida a consultar
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}, Partida: {partida}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_partidas_template(
|
||||
username=username,
|
||||
password=password,
|
||||
aduana=aduana,
|
||||
patente=patente,
|
||||
pedimento=pedimento,
|
||||
numero_operacion=numero_operacion,
|
||||
partida=partida
|
||||
)
|
||||
|
||||
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
|
||||
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8'
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla-ws-pedimentos/ConsultarPartidaService?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
# partida = get_partida_data(soap_response.text)
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
|
||||
_file_name = f"vu_PT_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{partida}.xml"
|
||||
|
||||
|
||||
# Aqui entra proceso de alonso
|
||||
|
||||
# respuesta = scraping_partida(xml)
|
||||
|
||||
# 1. Leer XML
|
||||
|
||||
# 2. Obtener Datos del XML
|
||||
|
||||
# 3. Subir los datos a la base datos
|
||||
|
||||
# 4. Generar una respues
|
||||
|
||||
# Enviar el documento XML como respuesta
|
||||
document_response = await rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=1
|
||||
)
|
||||
|
||||
|
||||
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_partidas: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar partidas: {str(e)}")
|
||||
|
||||
async def get_soap_acuse(credenciales, response_service, soap_controller, edocument, idx): # Testeado
|
||||
"""
|
||||
Procesa la petición SOAP para obtener el acuse de un pedimento y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_acuse_template(
|
||||
username=username,
|
||||
password=password,
|
||||
idEDocument=edocument['numero_edocument']
|
||||
)
|
||||
|
||||
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
|
||||
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseEdocument',# AcuseCove
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
# Extraer contenido Base64 del acuse
|
||||
logger.info("Extrayendo documento binario del acuse...")
|
||||
acuse_base64 = extract_acuse_documento_from_soap(soap_response.text)
|
||||
|
||||
|
||||
if not acuse_base64:
|
||||
logger.error("No se pudo extraer el contenido del acuseDocumento")
|
||||
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
|
||||
|
||||
# Decodificar contenido Base64
|
||||
logger.info("Decodificando contenido Base64...")
|
||||
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="No se pudo decodificar el documento del acuse")
|
||||
|
||||
# Verificar que es un PDF válido
|
||||
if not pdf_bytes.startswith(b'%PDF'):
|
||||
logger.warning("El contenido decodificado no parece ser un PDF válido")
|
||||
# Continuar de todos modos, podría ser otro tipo de documento
|
||||
|
||||
# Generar nombre del archivo
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
_file_name = f"vu_AC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
|
||||
|
||||
# Enviar el documento PDF usando binary_content
|
||||
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
|
||||
document_response = await rest_controller.post_document(
|
||||
binary_content=pdf_bytes,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=4
|
||||
)
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_acuse: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse: {str(e)}")
|
||||
|
||||
async def get_soap_acuseCOVE(credenciales, response_service, soap_controller, cove, idx): # Testeado
|
||||
"""
|
||||
Procesa la petición SOAP para obtener el acuse COVE de un pedimento y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_acuse_template(
|
||||
username=username,
|
||||
password=password,
|
||||
idEDocument=cove['numero_cove']
|
||||
)
|
||||
|
||||
### >>> AQUÍ SE AÑADE EL LOGGER.DEBUG <<< ###
|
||||
logger.debug(f"XML SOAP generado: {soap_xml}") # 👈 Registra el XML completo
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'http://www.ventanillaunica.gob.mx/ventanilla/ConsultaAcusesService/consultarAcuseCove',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"ventanilla-acuses-HA/ConsultaAcusesServiceWS?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
# Extraer contenido Base64 del acuse
|
||||
logger.info("Extrayendo documento binario del acuse cove...")
|
||||
acuse_base64 = extract_acuse_documento_from_soap(soap_response.text)
|
||||
|
||||
|
||||
if not acuse_base64:
|
||||
logger.error("No se pudo extraer el contenido del acuseDocumento")
|
||||
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
|
||||
|
||||
# Decodificar contenido Base64
|
||||
logger.info("Decodificando contenido Base64...")
|
||||
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="No se pudo decodificar el documento del acuse")
|
||||
|
||||
# Verificar que es un PDF válido
|
||||
if not pdf_bytes.startswith(b'%PDF'):
|
||||
logger.warning("El contenido decodificado no parece ser un PDF válido")
|
||||
# Continuar de todos modos, podría ser otro tipo de documento
|
||||
|
||||
# Generar nombre del archivo
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
_file_name = f"vu_AC_COVE_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
|
||||
|
||||
# Enviar el documento PDF usando binary_content
|
||||
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
|
||||
document_response = await rest_controller.post_document(
|
||||
binary_content=pdf_bytes,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=7
|
||||
)
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_acuse cove: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse cove: {str(e)}")
|
||||
|
||||
async def get_estado_pedimento(credenciales, response_service, soap_controller): # Sin testear
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_estado_pedimento_template(
|
||||
username=username,
|
||||
password=password,
|
||||
aduana=aduana,
|
||||
patente=patente,
|
||||
pedimento=pedimento,
|
||||
numero_operacion=numero_operacion
|
||||
)
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8'
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request_async(
|
||||
"webservice-pedimentos-HA/consultarEstadoPedimento?wsdl",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
data = xml_controller.extract_data(soap_response.text)
|
||||
remesas = 1 if data.get('remesas', 0) else 2
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = data.get('numero_partidas', 0)
|
||||
tipo_operacion = data.get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
|
||||
_file_name = f"vu_EP_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}.xml"
|
||||
# Enviar el documento XML como respuesta
|
||||
document_response = await rest_controller.post_document(
|
||||
soap_response=soap_response,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=6
|
||||
)
|
||||
|
||||
data['organizacion'] = response_service['organizacion']
|
||||
data['id'] = response_service['pedimento']['id']
|
||||
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response,
|
||||
"xml_content": data
|
||||
}
|
||||
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_pedimento_completo: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar pedimento completo: {str(e)}")
|
||||
|
||||
async def get_soap_edocument(credenciales, response_service, soap_controller, edocument, idx):
|
||||
"""
|
||||
Procesa la petición SOAP para obtener el acuse de un pedimento y guarda el documento.
|
||||
|
||||
Args:
|
||||
credenciales: Diccionario con credenciales VUCEM (usuario, password)
|
||||
response_service: Respuesta del servicio con datos del pedimento
|
||||
soap_controller: Instancia del controlador SOAP
|
||||
|
||||
Returns:
|
||||
dict: Respuesta con el servicio, respuesta SOAP y documento guardado
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay errores en la petición SOAP o al guardar el documento
|
||||
"""
|
||||
try:
|
||||
# Extraer credenciales
|
||||
username, password, aduana, patente, pedimento, numero_operacion = validate_pedimento_data(response_service, credenciales)
|
||||
|
||||
logger.info(f"Datos para SOAP - Usuario: {username}, Aduana: {aduana}, Patente: {patente}, Pedimento: {pedimento}, Numero Operacion: {numero_operacion}")
|
||||
|
||||
# Generar template SOAP
|
||||
soap_xml = soap_controller.generate_edocument_template(
|
||||
username=username,
|
||||
password=password,
|
||||
idEDocument=edocument['numero_edocument']
|
||||
)
|
||||
|
||||
# Realizar petición SOAP
|
||||
logger.info("Realizando petición SOAP...")
|
||||
|
||||
# Headers específicos para este servicio SOAP
|
||||
soap_headers = {
|
||||
'Content-Type': 'text/xml; charset=utf-8',
|
||||
'SOAPAction': 'http://tempuri.org/IServicioEdocument/GetDocumento',
|
||||
'Accept-Encoding': 'gzip,deflate',
|
||||
}
|
||||
|
||||
soap_response = await soap_controller.make_request(
|
||||
"Ventanilla-HA/ServicioEdocument/ServicioEdocument.svc",
|
||||
data=soap_xml,
|
||||
headers=soap_headers
|
||||
)
|
||||
|
||||
|
||||
if (soap_response) and (not soap_error(soap_response)):
|
||||
logger.info(f"Petición SOAP exitosa - Status: {soap_response.status_code}")
|
||||
|
||||
# Extraer contenido Base64 del acuse
|
||||
logger.info("Extrayendo documento binario del edocument...")
|
||||
response = extract_pdf_bytes_from_xml(soap_response.text)
|
||||
pdf_bytes = response.get('pdf_bytes')
|
||||
# cadena_original = response.get('cadena_original')
|
||||
# sello_digital = response.get('sello_digital')
|
||||
|
||||
if not acuse_base64:
|
||||
logger.error("No se pudo extraer el contenido del acuseDocumento")
|
||||
raise HTTPException(status_code=500, detail="No se pudo extraer el documento del acuse")
|
||||
|
||||
# Decodificar contenido Base64
|
||||
|
||||
response_edoc = rest_controller.put_edocument(edocument_id=ide, data={
|
||||
"numero_edocument": edocument['numero_edocument'],
|
||||
"pedimento": response_service['pedimento'],
|
||||
# "cadena_original": cadena_original,
|
||||
# "sello_digital": sello_digital
|
||||
})
|
||||
if not pdf_bytes:
|
||||
logger.error("No se pudo decodificar el contenido Base64 del acuse")
|
||||
raise HTTPException(status_code=500, detail="No se pudo decodificar el documento del acuse")
|
||||
|
||||
# Verificar que es un PDF válido
|
||||
if not pdf_bytes.startswith(b'%PDF'):
|
||||
logger.warning("El contenido decodificado no parece ser un PDF válido")
|
||||
# Continuar de todos modos, podría ser otro tipo de documento
|
||||
|
||||
# Generar nombre del archivo
|
||||
remesas = 1 if response_service['pedimento'].get('remesas', 0) else 0
|
||||
patente = response_service['pedimento'].get('patente', 'N/A')
|
||||
aduana = response_service['pedimento'].get('aduana', 'N/A')
|
||||
no_partidas = response_service['pedimento'].get('numero_partidas', 0)
|
||||
tipo_operacion = response_service['pedimento'].get('tipo_operacion', 'N/A')
|
||||
pedimento = response_service['pedimento'].get('pedimento', 'N/A')
|
||||
_file_name = f"vu_EDC_{remesas}{no_partidas}{tipo_operacion}_{aduana}_{patente}_{pedimento}_{idx}.pdf"
|
||||
|
||||
# Enviar el documento PDF usando binary_content
|
||||
logger.info(f"Enviando documento PDF: {_file_name} ({len(pdf_bytes)} bytes)")
|
||||
document_response = await rest_controller.post_document(
|
||||
binary_content=pdf_bytes,
|
||||
organizacion=response_service['organizacion'],
|
||||
pedimento=response_service['pedimento']['id'],
|
||||
file_name=_file_name,
|
||||
document_type=5
|
||||
)
|
||||
return {
|
||||
"servicio": response_service,
|
||||
"documento": document_response
|
||||
}
|
||||
else:
|
||||
logger.error("Error en petición SOAP")
|
||||
raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
|
||||
except HTTPException:
|
||||
# Re-lanzar HTTPExceptions sin modificar
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error inesperado en get_acuse: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error interno al procesar acuse: {str(e)}")
|
||||
|
||||
|
||||
558
utils/servicios.py
Normal file
558
utils/servicios.py
Normal file
@@ -0,0 +1,558 @@
|
||||
from core.config import settings
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from schemas.pedimentoSchema import PedimentoRequest
|
||||
from schemas.serviceSchema import ServiceBaseSchema, ServiceRemesaSchema
|
||||
import asyncio
|
||||
import logging
|
||||
logger = logging.getLogger("app.api")
|
||||
import traceback
|
||||
from typing import Dict, Any, List, Optional
|
||||
from contextlib import asynccontextmanager
|
||||
from controllers.RESTController import rest_controller
|
||||
from controllers.SOAPController import soap_controller
|
||||
from utils.peticiones import get_soap_pedimento_completo, get_soap_remesas, get_soap_partidas, get_soap_acuse, get_soap_edocument
|
||||
from fastapi.responses import JSONResponse
|
||||
from core.config import settings
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ESTADO_CREADO = 1
|
||||
ESTADO_EN_PROCESO = 2
|
||||
ESTADO_FINALIZADO = 3
|
||||
ESTADO_ERROR = 4
|
||||
|
||||
async def _validate_request_data(request_data: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Valida los datos básicos requeridos en las peticiones.
|
||||
|
||||
Args:
|
||||
request_data: Diccionario con datos de la petición
|
||||
|
||||
Raises:
|
||||
HTTPException: Si faltan datos requeridos
|
||||
"""
|
||||
if not request_data.get('pedimento'):
|
||||
logger.error("ID del pedimento no proporcionado en la petición")
|
||||
raise HTTPException(status_code=400, detail="ID del pedimento es requerido")
|
||||
|
||||
if not request_data.get('organizacion'):
|
||||
logger.error("ID de la organización no proporcionado en la petición")
|
||||
raise HTTPException(status_code=400, detail="ID de la organización es requerido")
|
||||
|
||||
logger.info(f"Validación exitosa - Pedimento: {request_data['pedimento']}, Organización: {request_data['organizacion']}")
|
||||
|
||||
async def _get_pedimento_service(pedimento_id: str, service_type: int, operation_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Obtiene el servicio de pedimento por tipo.
|
||||
|
||||
Args:
|
||||
pedimento_id: ID del pedimento
|
||||
service_type: Tipo de servicio a obtener
|
||||
operation_name: Nombre de la operación para logging
|
||||
|
||||
Returns:
|
||||
Dict con datos del servicio
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay error al obtener el servicio
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Obteniendo servicio tipo {service_type} para pedimento {pedimento_id} - Operación: {operation_name}")
|
||||
response_service = await rest_controller.get_pedimento_services(pedimento_id, service_type=service_type)
|
||||
logger.info(response_service)
|
||||
|
||||
if not response_service or len(response_service) == 0:
|
||||
logger.error(f"No se encontró servicio tipo {service_type} para pedimento {pedimento_id}")
|
||||
raise HTTPException(status_code=404, detail=f"No se encontró servicio de {operation_name}")
|
||||
|
||||
logger.info(f"Servicio obtenido exitosamente: {response_service[0].get('id', 'N/A')}")
|
||||
return response_service[0]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener servicio de {operation_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"Error al obtener servicio de {operation_name}")
|
||||
|
||||
async def _get_vucem_credentials(contribuyente_id: str, operation_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Obtiene las credenciales VUCEM para un contribuyente.
|
||||
|
||||
Args:
|
||||
contribuyente_id: ID del contribuyente
|
||||
operation_name: Nombre de la operación para logging
|
||||
|
||||
Returns:
|
||||
Dict con credenciales VUCEM
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay error al obtener credenciales
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Obteniendo credenciales VUCEM para contribuyente {contribuyente_id} - Operación: {operation_name}")
|
||||
response_credentials = await rest_controller.get_vucem_credentials(contribuyente_id)
|
||||
|
||||
if not response_credentials or len(response_credentials) == 0:
|
||||
logger.error(f"No se encontraron credenciales VUCEM para contribuyente {contribuyente_id}")
|
||||
raise HTTPException(status_code=404, detail="Credenciales VUCEM no encontradas")
|
||||
|
||||
logger.info("Credenciales VUCEM obtenidas exitosamente")
|
||||
return response_credentials[0]
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error al obtener credenciales VUCEM para {operation_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail="Error al obtener credenciales VUCEM")
|
||||
|
||||
async def _post_edocuments(response_service: dict, identificadores_ed: list):
|
||||
"""
|
||||
Helper function para enviar documentos digitalizados a la API.
|
||||
|
||||
Args:
|
||||
response_service: Diccionario con datos del servicio
|
||||
identificadores_ed: Lista de identificadores ED a enviar
|
||||
"""
|
||||
responses = []
|
||||
|
||||
for identificador in identificadores_ed:
|
||||
# Preparar datos del documento
|
||||
document_data = {
|
||||
'clave': identificador['clave'],
|
||||
'descripcion': identificador['descripcion'],
|
||||
'numero_edocument': identificador['complemento1'],
|
||||
'organizacion': response_service['organizacion'],
|
||||
'pedimento': response_service['pedimento']['id']
|
||||
}
|
||||
|
||||
try:
|
||||
response = await rest_controller.post_edocument(document_data)
|
||||
if response is None:
|
||||
logger.warning(f"No se pudo enviar el documento {identificador['complemento1']}")
|
||||
continue
|
||||
responses.append(response)
|
||||
logger.info(f"Documento {identificador['complemento1']} enviado exitosamente")
|
||||
except Exception as e:
|
||||
logger.error(f"Error al enviar el documento {identificador['complemento1']}: {e}")
|
||||
continue
|
||||
|
||||
if not responses:
|
||||
raise HTTPException(status_code=500, detail="No se pudo enviar ningún documento digitalizado")
|
||||
|
||||
return responses
|
||||
|
||||
async def _post_coves(response_service: dict, coves: list) -> List[Dict[str, Any]]:
|
||||
responses = []
|
||||
|
||||
for cove in coves:
|
||||
# Preparar datos del documento
|
||||
document_data = {
|
||||
'numero_cove': cove,
|
||||
'organizacion': response_service['organizacion'],
|
||||
'pedimento': response_service['pedimento']['id']
|
||||
}
|
||||
|
||||
try:
|
||||
response = await rest_controller.post_cove(document_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error al enviar el numero de cove {cove}: {e}")
|
||||
continue
|
||||
|
||||
if not responses:
|
||||
raise HTTPException(status_code=500, detail="No se pudo enviar ningún numero de cove")
|
||||
|
||||
return responses
|
||||
|
||||
async def _update_service_status(service_id: int, estado: int, response_service: dict, operation_name: str = "operación") -> bool:
|
||||
"""
|
||||
Actualiza el estado del servicio de manera robusta.
|
||||
|
||||
Args:
|
||||
service_id: ID del servicio
|
||||
estado: Nuevo estado (1=creado, 2=en proceso, 3=finalizado, 4=error)
|
||||
response_service: Datos del servicio
|
||||
operation_name: Nombre de la operación para logging
|
||||
|
||||
Returns:
|
||||
bool: True si se actualizó exitosamente, False en caso contrario
|
||||
"""
|
||||
estado_nombres = {
|
||||
1: "CREADO",
|
||||
2: "EN_PROCESO",
|
||||
3: "FINALIZADO",
|
||||
4: "ERROR"
|
||||
}
|
||||
|
||||
estado_nombre = estado_nombres.get(estado, f"DESCONOCIDO({estado})")
|
||||
|
||||
try:
|
||||
logger.info(f"Actualizando estado del servicio {service_id} a {estado_nombre} - Operación: {operation_name}")
|
||||
|
||||
update_data = {
|
||||
"estado": estado,
|
||||
"pedimento": response_service['pedimento']['id'],
|
||||
"organizacion": response_service['organizacion'],
|
||||
}
|
||||
logger.info(f"Body enviado al endpoint PUT: {update_data}")
|
||||
print(f"Body enviado al endpoint PUT: {update_data}")
|
||||
result = await rest_controller.put_pedimento_service(service_id=service_id, data=update_data)
|
||||
|
||||
if result is None:
|
||||
logger.error(f"Falló la actualización del estado del servicio {service_id} a {estado_nombre}")
|
||||
return False
|
||||
|
||||
logger.info(f"Estado del servicio {service_id} actualizado exitosamente a {estado_nombre}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al actualizar estado del servicio {service_id} a {estado_nombre} - Operación {operation_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
async def _create_response(service_data: dict, additional_data: Optional[Dict[str, Any]] = None,
|
||||
success_message: str = "Operación completada exitosamente") -> Dict[str, Any]:
|
||||
"""
|
||||
Crea una respuesta estandarizada para los endpoints.
|
||||
|
||||
Args:
|
||||
service_data: Datos del servicio
|
||||
additional_data: Datos adicionales a incluir en la respuesta
|
||||
success_message: Mensaje de éxito personalizado
|
||||
|
||||
Returns:
|
||||
Dict con estructura de respuesta estandarizada
|
||||
"""
|
||||
response = {
|
||||
"success": True,
|
||||
"message": success_message,
|
||||
"data": {
|
||||
"organizacion": service_data['organizacion'],
|
||||
"servicio": service_data['id'],
|
||||
"estado": ESTADO_FINALIZADO,
|
||||
"pedimento_id": service_data['pedimento']['id']
|
||||
}
|
||||
}
|
||||
|
||||
if additional_data:
|
||||
response["data"].update(additional_data)
|
||||
|
||||
logger.info(f"Respuesta creada exitosamente para servicio {service_data['id']}")
|
||||
return response
|
||||
|
||||
async def _execute_service_safely(service_func, request_data: Dict[str, Any], service_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Ejecuta un servicio de manera segura capturando errores.
|
||||
|
||||
Args:
|
||||
service_func: Función del servicio a ejecutar
|
||||
request_data: Datos para la petición
|
||||
service_name: Nombre del servicio para logging
|
||||
|
||||
Returns:
|
||||
Dict con resultado de la ejecución
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Iniciando ejecución automática de {service_name}...")
|
||||
|
||||
# Crear el objeto request apropiado
|
||||
from schemas.serviceSchema import ServiceRemesaSchema
|
||||
request_obj = ServiceRemesaSchema(**request_data)
|
||||
|
||||
# Ejecutar el servicio
|
||||
result = await service_func(request_obj)
|
||||
|
||||
logger.info(f"Servicio {service_name} ejecutado exitosamente")
|
||||
return {
|
||||
"success": True,
|
||||
"service_name": service_name,
|
||||
"result": result.body.decode() if hasattr(result, 'body') else str(result),
|
||||
"status_code": result.status_code if hasattr(result, 'status_code') else 200
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en ejecución automática de {service_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return {
|
||||
"success": False,
|
||||
"service_name": service_name,
|
||||
"error": str(e),
|
||||
"status_code": 500
|
||||
}
|
||||
|
||||
async def _execute_service_with_retry(service_func, request_data: Dict[str, Any],
|
||||
service_name: str, max_retries: int = 2) -> Dict[str, Any]:
|
||||
"""
|
||||
Ejecuta un servicio con reintentos automáticos en caso de fallo.
|
||||
|
||||
Args:
|
||||
service_func: Función del servicio a ejecutar
|
||||
request_data: Datos para la petición
|
||||
service_name: Nombre del servicio para logging
|
||||
max_retries: Número máximo de reintentos
|
||||
|
||||
Returns:
|
||||
Dict con resultado de la ejecución
|
||||
"""
|
||||
last_error = None
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
if attempt > 0:
|
||||
wait_time = min(2 ** attempt, 30) # Backoff exponencial, máximo 30 segundos
|
||||
logger.info(f"Reintentando {service_name} en {wait_time} segundos (intento {attempt + 1}/{max_retries + 1})")
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
result = await _execute_service_safely(service_func, request_data, service_name)
|
||||
|
||||
if result["success"]:
|
||||
if attempt > 0:
|
||||
logger.info(f"✅ Servicio {service_name} exitoso en intento {attempt + 1}")
|
||||
return result
|
||||
else:
|
||||
last_error = result.get("error", "Error desconocido")
|
||||
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
logger.warning(f"Intento {attempt + 1} fallido para {service_name}: {e}")
|
||||
|
||||
# Si llegamos aquí, todos los intentos fallaron
|
||||
logger.error(f"❌ Servicio {service_name} falló después de {max_retries + 1} intentos. Último error: {last_error}")
|
||||
return {
|
||||
"success": False,
|
||||
"service_name": service_name,
|
||||
"error": f"Falló después de {max_retries + 1} intentos. Último error: {last_error}",
|
||||
"status_code": 500,
|
||||
"retries_attempted": max_retries + 1
|
||||
}
|
||||
|
||||
async def _wait_for_service_creation(pedimento_id: str, service_type: int,
|
||||
timeout: int = 60, check_interval: int = 2) -> bool:
|
||||
"""
|
||||
Espera a que un servicio sea creado antes de intentar ejecutarlo.
|
||||
|
||||
Args:
|
||||
pedimento_id: ID del pedimento
|
||||
service_type: Tipo de servicio a esperar
|
||||
timeout: Tiempo máximo de espera en segundos
|
||||
check_interval: Intervalo entre verificaciones en segundos
|
||||
|
||||
Returns:
|
||||
bool: True si el servicio fue encontrado, False si se agotó el timeout
|
||||
"""
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
logger.info(f"Esperando creación de servicio tipo {service_type} para pedimento {pedimento_id} (timeout: {timeout}s)")
|
||||
|
||||
attempt = 0
|
||||
while (asyncio.get_event_loop().time() - start_time) < timeout:
|
||||
try:
|
||||
attempt += 1
|
||||
services = await rest_controller.get_pedimento_services(pedimento_id, service_type=service_type)
|
||||
|
||||
if services and len(services) > 0:
|
||||
logger.info(f"✅ Servicio tipo {service_type} encontrado para pedimento {pedimento_id} (intento {attempt})")
|
||||
return True
|
||||
else:
|
||||
if attempt % 10 == 0: # Log cada 20 segundos aprox
|
||||
logger.info(f"⏳ Servicio tipo {service_type} aún no encontrado para pedimento {pedimento_id} (intento {attempt}/{timeout//check_interval})")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error verificando servicio tipo {service_type}: {e}")
|
||||
|
||||
await asyncio.sleep(check_interval)
|
||||
|
||||
logger.error(f"❌ Timeout esperando servicio tipo {service_type} para pedimento {pedimento_id} después de {timeout}s")
|
||||
return False
|
||||
|
||||
async def _execute_follow_up_services(pedimento_id: str, organizacion_id: str,
|
||||
has_remesas: bool = False, has_partidas: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Ejecuta automáticamente los servicios de seguimiento después del pedimento completo.
|
||||
|
||||
Args:
|
||||
pedimento_id: ID del pedimento
|
||||
organizacion_id: ID de la organización
|
||||
has_remesas: Si el pedimento tiene remesas
|
||||
has_partidas: Si el pedimento tiene partidas
|
||||
|
||||
Returns:
|
||||
Dict con resultados de la ejecución
|
||||
"""
|
||||
logger.info(f"Iniciando ejecución automática de servicios para pedimento {pedimento_id}")
|
||||
|
||||
request_data = {
|
||||
"pedimento": pedimento_id,
|
||||
"organizacion": organizacion_id
|
||||
}
|
||||
|
||||
# Lista de servicios a ejecutar con sus tipos correspondientes
|
||||
services_to_execute = []
|
||||
|
||||
# Agregar partidas si el pedimento las tiene
|
||||
if has_partidas:
|
||||
services_to_execute.append(("partidas", get_partidas, 4))
|
||||
|
||||
# Agregar remesas si el pedimento las tiene
|
||||
if has_remesas:
|
||||
services_to_execute.append(("remesas", get_remesas, 5))
|
||||
|
||||
# Siempre agregar acuses (si existen documentos digitalizados)
|
||||
services_to_execute.append(("acuse", get_acuse, 6))
|
||||
|
||||
# Resultados de ejecución
|
||||
execution_results = {
|
||||
"total_services": len(services_to_execute),
|
||||
"successful_services": 0,
|
||||
"failed_services": 0,
|
||||
"results": []
|
||||
}
|
||||
|
||||
# Esperar un poco antes de iniciar para que se completen los servicios creados
|
||||
logger.info("Esperando a que se completen las creaciones de servicios...")
|
||||
await asyncio.sleep(10) # Aumentado de 5 a 10 segundos
|
||||
|
||||
# Ejecutar servicios secuencialmente para evitar sobrecarga
|
||||
for service_name, service_func, service_type in services_to_execute:
|
||||
try:
|
||||
logger.info(f"🔄 Iniciando procesamiento de {service_name}...")
|
||||
|
||||
# Verificar que el servicio exista antes de ejecutar
|
||||
service_exists = await _wait_for_service_creation(pedimento_id, service_type, timeout=60)
|
||||
|
||||
if not service_exists:
|
||||
execution_results["failed_services"] += 1
|
||||
execution_results["results"].append({
|
||||
"success": False,
|
||||
"service_name": service_name,
|
||||
"error": f"Servicio tipo {service_type} no encontrado después de esperar",
|
||||
"status_code": 404
|
||||
})
|
||||
logger.warning(f"⚠️ Servicio {service_name} no encontrado, saltando...")
|
||||
continue
|
||||
|
||||
# Ejecutar servicio con reintentos
|
||||
result = await _execute_service_with_retry(service_func, request_data, service_name, max_retries=2)
|
||||
execution_results["results"].append(result)
|
||||
|
||||
if result["success"]:
|
||||
execution_results["successful_services"] += 1
|
||||
logger.info(f"✅ Servicio {service_name} completado exitosamente")
|
||||
else:
|
||||
execution_results["failed_services"] += 1
|
||||
logger.warning(f"❌ Servicio {service_name} falló: {result.get('error', 'Error desconocido')}")
|
||||
|
||||
# Esperar entre servicios para no sobrecargar
|
||||
await asyncio.sleep(3)
|
||||
|
||||
except Exception as e:
|
||||
execution_results["failed_services"] += 1
|
||||
execution_results["results"].append({
|
||||
"success": False,
|
||||
"service_name": service_name,
|
||||
"error": f"Error crítico: {str(e)}",
|
||||
"status_code": 500
|
||||
})
|
||||
logger.error(f"💥 Error crítico en servicio {service_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Log de resumen
|
||||
success_rate = (execution_results["successful_services"] / execution_results["total_services"]) * 100 if execution_results["total_services"] > 0 else 0
|
||||
|
||||
if execution_results["successful_services"] == execution_results["total_services"]:
|
||||
logger.info(f"🎉 Ejecución automática completada exitosamente - {execution_results['successful_services']}/{execution_results['total_services']} (100%)")
|
||||
else:
|
||||
logger.warning(f"⚠️ Ejecución automática completada con errores - Éxito: {execution_results['successful_services']}/{execution_results['total_services']} ({success_rate:.1f}%)")
|
||||
|
||||
return execution_results
|
||||
|
||||
async def _schedule_follow_up_services(pedimento_id: str, organizacion_id: str,
|
||||
xml_content: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Programa la ejecución de servicios de seguimiento en segundo plano.
|
||||
|
||||
Args:
|
||||
pedimento_id: ID del pedimento
|
||||
organizacion_id: ID de la organización
|
||||
xml_content: Contenido XML del pedimento para determinar qué servicios ejecutar
|
||||
"""
|
||||
try:
|
||||
# Determinar qué servicios ejecutar basado en el contenido del pedimento
|
||||
has_remesas = bool(xml_content.get('remesas', 0))
|
||||
has_partidas = xml_content.get('numero_partidas', 0) > 0
|
||||
|
||||
logger.info(f"Programando servicios automáticos - Remesas: {has_remesas}, Partidas: {has_partidas}")
|
||||
|
||||
# Crear tarea en segundo plano
|
||||
task = asyncio.create_task(
|
||||
_execute_follow_up_services(
|
||||
pedimento_id=pedimento_id,
|
||||
organizacion_id=organizacion_id,
|
||||
has_remesas=has_remesas,
|
||||
has_partidas=has_partidas
|
||||
)
|
||||
)
|
||||
|
||||
# Agregar callback para logging cuando termine
|
||||
def log_completion(task):
|
||||
try:
|
||||
result = task.result()
|
||||
logger.info(f"Servicios automáticos completados para pedimento {pedimento_id}: {result['successful_services']}/{result['total_services']} exitosos")
|
||||
except Exception as e:
|
||||
logger.error(f"Error en servicios automáticos para pedimento {pedimento_id}: {e}")
|
||||
|
||||
task.add_done_callback(log_completion)
|
||||
|
||||
logger.info(f"Servicios automáticos programados exitosamente para pedimento {pedimento_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error al programar servicios automáticos: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
|
||||
logger.error(f"Error inesperado en {operation_name}: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
# Actualizar estado a error si tenemos service_data
|
||||
if service_data:
|
||||
try:
|
||||
await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
|
||||
except Exception as update_error:
|
||||
logger.error(f"Error al actualizar estado del servicio tras fallo: {update_error}")
|
||||
|
||||
raise HTTPException(status_code=500, detail=f"Error interno en {operation_name}: {str(e)}")
|
||||
|
||||
def _log_operation_summary(operation_name: str, service_id: int, success: bool,
|
||||
additional_info: Optional[str] = None) -> None:
|
||||
"""
|
||||
Registra un resumen de la operación realizada.
|
||||
|
||||
Args:
|
||||
operation_name: Nombre de la operación
|
||||
service_id: ID del servicio procesado
|
||||
success: Si la operación fue exitosa
|
||||
additional_info: Información adicional opcional
|
||||
"""
|
||||
status = "EXITOSO" if success else "FALLIDO"
|
||||
message = f"RESUMEN {operation_name.upper()}: {status} - Servicio ID: {service_id}"
|
||||
|
||||
if additional_info:
|
||||
message += f" - {additional_info}"
|
||||
|
||||
if success:
|
||||
logger.info(message)
|
||||
else:
|
||||
logger.error(message)
|
||||
|
||||
async def _validate_soap_controller() -> None:
|
||||
"""
|
||||
Valida que el controlador SOAP esté disponible.
|
||||
|
||||
Raises:
|
||||
HTTPException: Si el controlador SOAP no está disponible
|
||||
"""
|
||||
if not soap_controller:
|
||||
logger.error("Controlador SOAP no disponible")
|
||||
raise HTTPException(status_code=500, detail="Servicio SOAP no disponible")
|
||||
Reference in New Issue
Block a user