Primera version estable de microservicios

This commit is contained in:
2025-07-28 11:04:18 -06:00
parent 42a564cd74
commit 5f58fabcfe
37 changed files with 5079 additions and 0 deletions

72
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
# App package

1
api/__init__.py Normal file
View File

@@ -0,0 +1 @@
# API package

1
api/api_v1/__init__.py Normal file
View File

@@ -0,0 +1 @@
# API v1 package

14
api/api_v1/api.py Normal file
View 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"])

View File

@@ -0,0 +1 @@
# Endpoints package

View 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"
}

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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
View File

1
core/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Core package

49
core/config.py Normal file
View 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()

View 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
View 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
View 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

View File

View File

View File

78
main.py Normal file
View 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

Binary file not shown.

21
requirements.txt Normal file
View 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
View File

8
schemas/acuseSchema.py Normal file
View 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
View 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
View 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
View 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
View File

439
test.xml Normal file
View 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
View File

884
utils/peticiones.py Normal file
View 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 &#xd;, &#xa;, 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
View 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")