diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..75e429a
--- /dev/null
+++ b/.dockerignore
@@ -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
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..f782fa3
--- /dev/null
+++ b/.env.example
@@ -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
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..662ac77
--- /dev/null
+++ b/.gitignore
@@ -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/*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ff04b49
--- /dev/null
+++ b/Dockerfile
@@ -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"]
\ No newline at end of file
diff --git a/Dockerfile.prod b/Dockerfile.prod
new file mode 100644
index 0000000..bd6c4fe
--- /dev/null
+++ b/Dockerfile.prod
@@ -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"]
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..edabda9
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1 @@
+# App package
diff --git a/api/__init__.py b/api/__init__.py
new file mode 100644
index 0000000..28b07ef
--- /dev/null
+++ b/api/__init__.py
@@ -0,0 +1 @@
+# API package
diff --git a/api/api_v1/__init__.py b/api/api_v1/__init__.py
new file mode 100644
index 0000000..6c2f33c
--- /dev/null
+++ b/api/api_v1/__init__.py
@@ -0,0 +1 @@
+# API v1 package
diff --git a/api/api_v1/api.py b/api/api_v1/api.py
new file mode 100644
index 0000000..30a1f6b
--- /dev/null
+++ b/api/api_v1/api.py
@@ -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"])
+
diff --git a/api/api_v1/endpoints/__init__.py b/api/api_v1/endpoints/__init__.py
new file mode 100644
index 0000000..d1feea4
--- /dev/null
+++ b/api/api_v1/endpoints/__init__.py
@@ -0,0 +1 @@
+# Endpoints package
diff --git a/api/api_v1/endpoints/health.py b/api/api_v1/endpoints/health.py
new file mode 100644
index 0000000..c53d0c2
--- /dev/null
+++ b/api/api_v1/endpoints/health.py
@@ -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"
+ }
diff --git a/api/api_v1/endpoints/pedimentos.py b/api/api_v1/endpoints/pedimentos.py
new file mode 100644
index 0000000..418e870
--- /dev/null
+++ b/api/api_v1/endpoints/pedimentos.py
@@ -0,0 +1,1352 @@
+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_acuseCOVE, get_soap_pedimento_completo, get_soap_remesas, get_soap_partidas, get_soap_acuse, get_soap_edocument
+from fastapi.responses import JSONResponse
+from utils.servicios import (
+ _validate_request_data,
+ _get_pedimento_service,
+ _update_service_status,
+ _get_vucem_credentials,
+ _create_response,
+ _post_edocuments,
+ _schedule_follow_up_services,
+ _post_coves
+)
+from core.config import settings
+
+from utils.servicios import *
+
+# Estados del servicio
+ESTADO_CREADO = 1
+ESTADO_EN_PROCESO = 2
+ESTADO_FINALIZADO = 3
+ESTADO_ERROR = 4
+
+router = APIRouter()
+logger = logging.getLogger(__name__)
+
+@router.post("/services/estado_pedimento")
+async def get_estado_pedimento(request: ServiceRemesaSchema):
+ """
+ Obtiene el estado actual de un pedimento mediante consulta SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de estado de pedimento existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Realiza petición SOAP para consultar estado
+ 5. Procesa y retorna información del estado
+
+ Args:
+ request: ServiceBaseSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con estado actual del pedimento
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "estado_pedimento"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando consulta de estado de pedimento - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de estado de pedimento existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=1,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Procesar petición SOAP para obtener estado del pedimento
+ logger.info("Realizando petición SOAP para estado del pedimento...")
+ try:
+ soap_response = await get_estado_pedimento(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller
+ )
+
+ if not soap_response:
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP para estado del pedimento")
+
+ logger.info("Petición SOAP para estado del pedimento completada exitosamente")
+
+ except HTTPException:
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise
+ except Exception as e:
+ logger.error(f"Error en petición SOAP para estado del pedimento: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "estado_pedimento": soap_response,
+ "documento": soap_response.get('documento', {}),
+ "xml_content": soap_response.get('xml_content', {})
+ },
+ success_message="Estado del pedimento consultado exitosamente"
+ )
+
+ logger.info(f"Consulta de estado de pedimento completada exitosamente - Servicio: {service_data['id']}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ pass
+ logger.error(f"Error inesperado en {operation_name}: {e}")
+
+@router.post("/services/listar_pedimentos")
+async def get_listar_pedimentos(request: ServiceRemesaSchema):
+ """
+ Lista pedimentos disponibles en el sistema VUCEM para una organización.
+
+ Este endpoint:
+ 1. Obtiene el servicio de listado de pedimentos existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Consulta lista de pedimentos en VUCEM
+ 5. Procesa y retorna la lista de pedimentos
+
+ Args:
+ request: ServiceBaseSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con lista de pedimentos disponibles
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "listar_pedimentos"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando listado de pedimentos - Organización: {request_data['organizacion']}")
+
+ # Obtener servicio de listado de pedimentos existente
+ # Nota: Asumiendo que existe un tipo de servicio para listado (tipo 8)
+ # Ajustar el tipo según la configuración del sistema
+ try:
+ services = await rest_controller.get_pedimento_services(
+ request_data['pedimento'],
+ service_type=8 # Tipo para listado de pedimentos
+ )
+
+ if not services or len(services) == 0:
+ logger.error(f"No se encontró servicio de listado de pedimentos")
+ raise HTTPException(status_code=404, detail="Servicio de listado no encontrado")
+
+ service_data = services[0]
+ logger.info(f"Servicio de listado obtenido: {service_data.get('id', 'N/A')}")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error al obtener servicio de listado: {e}")
+ raise HTTPException(status_code=500, detail="Error al obtener servicio de listado")
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Consultar pedimentos en VUCEM
+ logger.info("Consultando pedimentos disponibles en VUCEM...")
+ try:
+ # Nota: Este endpoint requiere implementar la función específica en utils/peticiones.py
+ # Por ahora, simularemos la respuesta básica
+
+ # TODO: Implementar get_soap_lista_pedimentos en utils/peticiones.py
+ # soap_response = await get_soap_lista_pedimentos(
+ # credenciales=credentials,
+ # response_service=service_data,
+ # soap_controller=soap_controller
+ # )
+
+ # Respuesta simulada para demostrar la estructura
+ soap_response = {
+ "pedimentos": [
+ {
+ "id": "PED001",
+ "numero": "24 44 1234 5678901",
+ "fecha": "2024-12-19",
+ "estado": "Tramitado",
+ "patente": "1234",
+ "aduana": "44"
+ }
+ ],
+ "total": 1,
+ "documento": {
+ "filename": "lista_pedimentos.xml",
+ "size": 1024
+ }
+ }
+
+ logger.info(f"Se encontraron {soap_response.get('total', 0)} pedimentos")
+
+ except Exception as e:
+ logger.error(f"Error en consulta SOAP de pedimentos: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error en la consulta SOAP al servicio VUCEM")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "pedimentos": soap_response.get('pedimentos', []),
+ "total_pedimentos": soap_response.get('total', 0),
+ "documento": soap_response.get('documento', {}),
+ "fecha_consulta": "2024-12-19T12:00:00Z"
+ },
+ success_message=f"Se encontraron {soap_response.get('total', 0)} pedimentos disponibles"
+ )
+
+ # Agregar advertencia si no se encontraron pedimentos
+ if soap_response.get('total', 0) == 0:
+ response_data["warnings"] = [
+ "No se encontraron pedimentos disponibles en el periodo consultado"
+ ]
+
+ logger.info(f"Listado de pedimentos completado - Total: {soap_response.get('total', 0)}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+@router.post("/services/pedimento_completo")
+async def get_pedimento_completo(request: ServiceBaseSchema):
+ """
+ Obtiene el pedimento completo de VUCEM y procesa todos los documentos asociados.
+
+ Este endpoint:
+ 1. Crea un servicio de pedimento completo
+ 2. Obtiene credenciales VUCEM
+ 3. Realiza petición SOAP para pedimento completo
+ 4. Actualiza datos del pedimento
+ 5. Procesa documentos digitalizados (e-documents)
+ 6. Crea servicios adicionales automáticamente
+
+ Args:
+ request: ServiceBaseSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con datos del pedimento completo y servicios creados
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ service_id = None
+ operation_name = "pedimento_completo"
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de pedimento completo - Pedimento: {request_data['pedimento']}")
+
+ # Crear servicio de pedimento completo
+ logger.info("Creando servicio de pedimento completo...")
+ try:
+ response_service = await rest_controller.post_pedimento_service(request_data)
+
+ if not response_service:
+ raise HTTPException(status_code=500, detail="No se pudo crear el servicio de pedimento completo")
+
+ service_id = response_service['id']
+ logger.info(f"Servicio creado exitosamente con ID: {service_id}")
+
+ except Exception as e:
+ logger.error(f"Error al crear servicio de pedimento completo: {e}")
+ raise HTTPException(status_code=500, detail="Error al crear el servicio de pedimento")
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_id, ESTADO_EN_PROCESO, response_service, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = response_service['pedimento']['contribuyente']
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Procesar petición SOAP para obtener pedimento completo
+ logger.info("Realizando petición SOAP para pedimento completo...")
+ try:
+ soap_response = await get_soap_pedimento_completo(
+ credenciales=credentials,
+ response_service=response_service,
+ soap_controller=soap_controller
+ )
+
+ if not soap_response:
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
+
+ logger.info("Petición SOAP completada exitosamente")
+
+ except HTTPException:
+ await _update_service_status(service_id, ESTADO_ERROR, response_service, operation_name)
+ raise
+ except Exception as e:
+ logger.error(f"Error en petición SOAP: {e}")
+ await _update_service_status(service_id, ESTADO_ERROR, response_service, operation_name)
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
+
+ # Actualizar datos del pedimento con información del XML
+ logger.info("Actualizando datos del pedimento...")
+ try:
+ xml_content = soap_response.get('xml_content', {})
+ if xml_content:
+ # Excluir 'identificadores_ed' del contenido a enviar
+ update_content = {k: v for k, v in xml_content.items() if k != 'identificadores_ed'}
+
+ pedimento_response = await rest_controller.put_pedimento(
+ response_service['pedimento']['id'],
+ update_content
+ )
+ logger.info("Pedimento actualizado exitosamente")
+ else:
+ logger.warning("No se recibió contenido XML para actualizar el pedimento")
+
+ except Exception as e:
+ logger.warning(f"No se pudo actualizar el pedimento (continuando proceso): {e}")
+ # No fallar todo el proceso por este error
+
+ # Procesar documentos digitalizados (e-documents)
+ edocuments_result = []
+ edocuments_error = None
+
+ # agregando edocuments
+ try:
+ identificadores_ed = xml_content.get('identificadores_ed', [])
+ if identificadores_ed:
+ logger.info(f"Procesando {len(identificadores_ed)} documentos digitalizados...")
+ edocuments_result = await _post_edocuments(
+ response_service=response_service,
+ identificadores_ed=identificadores_ed
+ )
+ logger.info(f"Se procesaron exitosamente {len(edocuments_result)} documentos digitalizados")
+ else:
+ logger.info("No se encontraron documentos digitalizados (identificadores ED)")
+
+ except Exception as e:
+ logger.error(f"Error al procesar documentos digitalizados: {e}")
+ edocuments_error = str(e)
+ # No fallar todo el proceso por este error
+
+ # Agregando coves
+ try:
+ coves = xml_content.get('coves', [])
+ logger.warning(f"COVEs encontrados: {coves}")
+ # Aquí podrías guardar el COVE en la base de datos o hacer algo con él
+
+ for cove in coves:
+ logger.warning(f"Procesando COVE: {cove}")
+ # Aquí podrías guardar el COVE en la base de datos o hacer algo con él
+ # Por ejemplo, podrías crear un servicio específico para manejar los coves
+ cove_result = await _post_coves(
+ response_service=response_service,
+ coves=coves
+ )
+ except Exception as e:
+ logger.error(f"Error al procesar COVEs: {e}")
+ # No fallar todo el proceso por este error
+ cove_result = None
+ # Crear servicios adicionales automáticamente
+ servicios_adicionales = {}
+ servicios_error = None
+
+ try:
+ logger.info("Creando servicios adicionales...")
+ new_service_base = {
+ "pedimento": response_service['pedimento']['id'],
+ "organizacion": response_service['organizacion'],
+ "estado": ESTADO_CREADO,
+ "tipo_procesamiento": 2,
+ }
+
+ # Mapeo de servicios a crear
+ servicios_config = [
+ (4, "partidas"), # Tipo 4 para partidas
+ (6, "acuse"), # Tipo 6 para acuse
+ (1, "estado_pedimento"), # Tipo 1 para estado_pedimento
+ (7, "edocument"),# Tipo 7 para edocument
+ ]
+
+ # Agregar servicio de remesas solo si el pedimento tiene remesas
+ if xml_content.get('remesas', 0):
+ servicios_config.append((5, "remesas"))
+
+ for servicio_tipo, servicio_nombre in servicios_config:
+ try:
+ logger.info(f"Creando servicio {servicio_nombre} (tipo {servicio_tipo})...")
+ new_service = {**new_service_base, "servicio": servicio_tipo}
+ service_response = await rest_controller.post_pedimento_service(new_service)
+
+ if service_response:
+ servicios_adicionales[f"servicio_{servicio_nombre}"] = service_response['id']
+ logger.info(f"✅ Servicio {servicio_nombre} (tipo {servicio_tipo}) creado exitosamente con ID: {service_response['id']}")
+ else:
+ logger.error(f"❌ No se pudo crear el servicio {servicio_nombre} (tipo {servicio_tipo}) - respuesta vacía")
+
+ except Exception as e:
+ logger.error(f"❌ Error al crear servicio {servicio_nombre} (tipo {servicio_tipo}): {e}")
+ logger.error(f"Traceback: {traceback.format_exc()}")
+
+ except Exception as e:
+ logger.error(f"Error al crear servicios adicionales: {e}")
+ servicios_error = str(e)
+ # No fallar todo el proceso por este error
+
+ # Log resumen de servicios creados
+ logger.info(f"📋 Resumen servicios creados: {len(servicios_adicionales)} de {len(servicios_config)} servicios")
+ for servicio_nombre, servicio_id in servicios_adicionales.items():
+ logger.info(f" ✅ {servicio_nombre}: ID {servicio_id}")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_id, ESTADO_FINALIZADO, response_service, operation_name)
+
+ # Programar servicios automáticos en segundo plano
+ logger.info("Programando ejecución automática de servicios de seguimiento...")
+ try:
+ await _schedule_follow_up_services(
+ pedimento_id=response_service['pedimento']['id'],
+ organizacion_id=response_service['organizacion'],
+ xml_content=xml_content
+ )
+ logger.info("Servicios automáticos programados exitosamente")
+ except Exception as e:
+ logger.warning(f"No se pudieron programar servicios automáticos: {e}")
+ # No fallar el proceso principal por esto
+
+ # Construir respuesta final
+ response_data = await _create_response(
+ service_data=response_service,
+ additional_data={
+ "documento": soap_response.get('documento', {}),
+ "xml_content": xml_content,
+ "edocuments": edocuments_result
+ },
+ success_message="Pedimento completo procesado exitosamente. Servicios automáticos programados."
+ )
+
+
+
+ logger.info(f"Pedimento completo procesado exitosamente - Servicio: {service_id}")
+ return JSONResponse(content=response_data, status_code=200)
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ logger.error(f"Error inesperado en {operation_name}: {e}")
+ logger.error(f"Traceback: {traceback.format_exc()}")
+
+ # Actualizar estado a error si tenemos el service_id
+ if service_id:
+ try:
+ # Necesitamos response_service para actualizar estado
+ if 'response_service' in locals():
+ await _update_service_status(service_id, ESTADO_ERROR, response_service, 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)}")
+
+@router.post("/services/partidas")
+async def get_partidas(request: ServiceRemesaSchema):
+ """
+ Obtiene todas las partidas de un pedimento mediante peticiones SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de partidas existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Procesa cada partida individualmente
+ 5. Guarda documentos XML de cada partida
+
+ Args:
+ request: ServiceRemesaSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con lista de partidas procesadas
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "partidas"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de partidas - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de partidas existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=4,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Procesar partidas
+ partidas_procesadas = []
+ numero_partidas = service_data['pedimento'].get('numero_partidas', 0)
+
+ logger.info(f"Procesando {numero_partidas} partidas...")
+
+ if numero_partidas <= 0:
+ logger.warning("El pedimento no tiene partidas para procesar")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=404, detail="No se encontraron partidas para el pedimento")
+
+ # Procesar cada partida individualmente
+ for partida_num in range(1, numero_partidas + 1):
+ try:
+ logger.info(f"Procesando partida {partida_num}/{numero_partidas}")
+
+ # Aqui obtiene el xml
+ soap_response = await get_soap_partidas(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller,
+ partida=str(partida_num)
+ )
+
+ if soap_response:
+ partidas_procesadas.append({
+ "numero": partida_num,
+ "procesada": True,
+ "documento": soap_response.get('documento', {})
+ })
+ logger.info(f"Partida {partida_num} procesada exitosamente")
+ else:
+ logger.warning(f"No se pudo procesar la partida {partida_num}")
+ partidas_procesadas.append({
+ "numero": partida_num,
+ "procesada": False,
+ "error": "Error en petición SOAP"
+ })
+
+ except Exception as e:
+ logger.error(f"Error al procesar partida {partida_num}: {e}")
+ partidas_procesadas.append({
+ "numero": partida_num,
+ "procesada": False,
+ "error": str(e)
+ })
+ # Continuar con las siguientes partidas
+ continue
+
+ # Verificar si se procesó al menos una partida
+ partidas_exitosas = [p for p in partidas_procesadas if p.get('procesada', False)]
+
+ if not partidas_exitosas:
+ logger.error("No se pudo procesar ninguna partida")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="No se pudo procesar ninguna partida")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "partidas": partidas_procesadas,
+ "total_partidas": numero_partidas,
+ "partidas_exitosas": len(partidas_exitosas),
+ "partidas_fallidas": len(partidas_procesadas) - len(partidas_exitosas)
+ },
+ success_message=f"Se procesaron {len(partidas_exitosas)}/{numero_partidas} partidas exitosamente"
+ )
+
+ # Agregar advertencias si hubo partidas fallidas
+ if len(partidas_exitosas) < numero_partidas:
+ response_data["warnings"] = [
+ f"Se procesaron solo {len(partidas_exitosas)} de {numero_partidas} partidas"
+ ]
+
+ logger.info(f"Procesamiento de partidas completado - Exitosas: {len(partidas_exitosas)}/{numero_partidas}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+@router.post("/services/remesas")
+async def get_remesas(request: ServiceRemesaSchema):
+ """
+ Obtiene las remesas de un pedimento mediante petición SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de remesas existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Realiza petición SOAP para remesas
+ 5. Guarda documento XML de remesas
+
+ Args:
+ request: ServiceRemesaSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con datos de remesas procesadas
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "remesas"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de remesas - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de remesas existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=5,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Procesar petición SOAP para remesas
+ logger.info("Realizando petición SOAP para remesas...")
+ try:
+ soap_response = await get_soap_remesas(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller
+ )
+
+ if not soap_response:
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP para remesas")
+
+ logger.info("Petición SOAP para remesas completada exitosamente")
+
+ except HTTPException:
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise
+ except Exception as e:
+ logger.error(f"Error en petición SOAP para remesas: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error en la petición SOAP al servicio VUCEM")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "remesas": soap_response,
+ "documento": soap_response.get('documento', {})
+ },
+ success_message="Remesas procesadas exitosamente"
+ )
+
+ logger.info(f"Procesamiento de remesas completado exitosamente - Servicio: {service_data['id']}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+@router.post("/services/acuse")
+async def get_acuse(request: ServiceRemesaSchema):
+ """
+ Obtiene los acuses de documentos digitalizados de un pedimento mediante peticiones SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de acuse existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Obtiene lista de documentos digitalizados (e-documents)
+ 5. Procesa cada documento para obtener su acuse en PDF
+ 6. Guarda cada PDF procesado
+
+ Args:
+ request: ServiceRemesaSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con lista de documentos digitalizados procesados
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "acuse"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de acuses - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de acuse existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=6,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Obtener documentos digitalizados (e-documents)
+ logger.info("Obteniendo documentos digitalizados...")
+ try:
+ edocs = await rest_controller.get_edocs(service_data['pedimento']['id'])
+
+ if not edocs:
+ logger.warning("No se encontraron documentos digitalizados para el pedimento")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=404, detail="No se encontraron documentos digitalizados para el pedimento")
+
+ logger.info(f"Se encontraron {len(edocs)} documentos digitalizados")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error al obtener documentos digitalizados: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error al obtener documentos digitalizados")
+
+ # Procesar acuses de documentos digitalizados
+ documentos_procesados = []
+ documentos_exitosos = 0
+
+ logger.info(f"Procesando acuses para {len(edocs)} documentos...")
+
+ for idx, edoc in enumerate(edocs):
+ documento_info = {
+ "clave": edoc.get('clave', 'N/A'),
+ "descripcion": edoc.get('descripcion', 'N/A'),
+ "numero_edocument": edoc.get('numero_edocument', 'N/A'),
+ "procesado": False,
+ "error": None
+ }
+
+ # Verificar que el documento tenga número de e-document
+ if not edoc.get('numero_edocument'):
+ logger.warning(f"Documento {idx + 1} no tiene numero_edocument, saltando...")
+ documento_info["error"] = "Sin número de e-document"
+ documentos_procesados.append(documento_info)
+ continue
+
+ try:
+ logger.info(f"Procesando acuse para documento {idx + 1}: {edoc['numero_edocument']}")
+
+ soap_response = await get_soap_acuse(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller,
+ edocument=edoc,
+ idx=idx + 1
+ )
+
+ if soap_response:
+ documento_info["procesado"] = True
+ documento_info["documento"] = soap_response.get('documento', {})
+ documentos_exitosos += 1
+ logger.info(f"Acuse del documento {idx + 1} procesado exitosamente")
+ else:
+ documento_info["error"] = "Error en petición SOAP"
+ logger.warning(f"No se pudo procesar el acuse del documento {idx + 1}")
+
+ except Exception as e:
+ logger.error(f"Error al procesar acuse del documento {idx + 1}: {e}")
+ documento_info["error"] = str(e)
+ # Continuar con los siguientes documentos
+
+ documentos_procesados.append(documento_info)
+
+ # Verificar si se procesó al menos un documento
+ if documentos_exitosos == 0:
+ logger.error("No se pudo procesar ningún acuse de documento digitalizado")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="No se pudo procesar ningún acuse de documento digitalizado")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "edocumentos": documentos_procesados,
+ "total_documentos": len(edocs),
+ "documentos_exitosos": documentos_exitosos,
+ "documentos_fallidos": len(edocs) - documentos_exitosos
+ },
+ success_message=f"Se procesaron {documentos_exitosos}/{len(edocs)} acuses de documentos exitosamente"
+ )
+
+ # Agregar advertencias si hubo documentos fallidos
+ if documentos_exitosos < len(edocs):
+ response_data["warnings"] = [
+ f"Se procesaron solo {documentos_exitosos} de {len(edocs)} documentos digitalizados"
+ ]
+
+ logger.info(f"Procesamiento de acuses completado - Exitosos: {documentos_exitosos}/{len(edocs)}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+@router.post("/services/edocument")
+async def get_edocument(request: ServiceRemesaSchema):
+ """
+ Obtiene y procesa todos los documentos digitalizados (e-documents) de un pedimento.
+
+ Este endpoint:
+ 1. Obtiene el servicio de documentos digitalizados existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Obtiene lista de documentos digitalizados
+ 5. Procesa cada documento para obtener su edocument
+ 6. Retorna lista de documentos procesados
+
+ Args:
+ request: PedimentoRequest con pedimento y organización
+
+ Returns:
+ JSONResponse con lista de documentos digitalizados procesados
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "edocument"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de e-documents - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de documentos digitalizados existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=7,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Obtener documentos digitalizados
+ logger.info("Obteniendo documentos digitalizados...")
+ try:
+ edocs = await rest_controller.get_edocs(service_data['pedimento']['id'])
+
+ if not edocs:
+ logger.warning("No se encontraron documentos digitalizados para el pedimento")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=404, detail="No se encontraron documentos digitalizados para el pedimento")
+
+ logger.info(f"Se encontraron {len(edocs)} documentos digitalizados")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error al obtener documentos digitalizados: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error al obtener documentos digitalizados")
+
+ # Procesar documentos digitalizados
+ documentos_procesados = []
+ documentos_exitosos = 0
+
+ logger.info(f"Procesando {len(edocs)} documentos digitalizados...")
+
+ for idx, edoc in enumerate(edocs):
+ documento_info = {
+ "clave": edoc.get('clave', 'N/A'),
+ "descripcion": edoc.get('descripcion', 'N/A'),
+ "numero_edocument": edoc.get('numero_edocument', 'N/A'),
+ "procesado": False,
+ "error": None
+ }
+
+ # Verificar que el documento tenga número de e-document
+ if not edoc.get('numero_edocument'):
+ logger.warning(f"Documento {idx + 1} no tiene numero_edocument, saltando...")
+ documento_info["error"] = "Sin número de e-document"
+ documentos_procesados.append(documento_info)
+ continue
+
+ try:
+ logger.info(f"Procesando e-document {idx + 1}: {edoc['numero_edocument']}")
+
+ # Procesar acuse del documento
+ soap_response = await get_soap_edocument(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller,
+ edocument=edoc,
+ idx=idx + 1
+ )
+
+ if soap_response:
+ documento_info["procesado"] = True
+ documento_info["documento"] = soap_response.get('documento', {})
+ documentos_exitosos += 1
+ logger.info(f"E-document {idx + 1} procesado exitosamente")
+ else:
+ documento_info["error"] = "Error en petición SOAP"
+ logger.warning(f"No se pudo procesar el e-document {idx + 1}")
+
+ except Exception as e:
+ logger.error(f"Error al procesar e-document {idx + 1}: {e}")
+ documento_info["error"] = str(e)
+ # Continuar con los siguientes documentos
+
+ documentos_procesados.append(documento_info)
+
+ # Verificar si se procesó al menos un documento
+ if documentos_exitosos == 0:
+ logger.error("No se pudo procesar ningún documento digitalizado")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="No se pudo procesar ningún documento digitalizado")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "edocumentos": documentos_procesados,
+ "total_documentos": len(edocs),
+ "documentos_exitosos": documentos_exitosos,
+ "documentos_fallidos": len(edocs) - documentos_exitosos
+ },
+ success_message=f"Se procesaron {documentos_exitosos}/{len(edocs)} documentos digitalizados exitosamente"
+ )
+
+ # Agregar advertencias si hubo documentos fallidos
+ if documentos_exitosos < len(edocs):
+ response_data["warnings"] = [
+ f"Se procesaron solo {documentos_exitosos} de {len(edocs)} documentos digitalizados"
+ ]
+
+ logger.info(f"Procesamiento de e-documents completado - Exitosos: {documentos_exitosos}/{len(edocs)}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+@router.post("/services/coves") # Sin Testear
+async def get_cove(request: ServiceRemesaSchema):
+
+ """
+ Obtiene las COVES de un pedimento mediante petición SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de COVES existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Realiza petición SOAP para COVES
+ 5. Guarda documento XML de COVES
+
+ Args:
+ request: ServiceRemesaSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con datos de COVES procesadas
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "COVES"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de COVES - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de remesas existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=8,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+
+
+@router.post("/services/acuseCove") # Sin Testear
+async def get_Acusecove(request: ServiceRemesaSchema):
+ """
+ Obtiene los acuses de COVE de un pedimento mediante peticiones SOAP a VUCEM.
+
+ Este endpoint:
+ 1. Obtiene el servicio de acuse existente
+ 2. Actualiza estado a "en proceso"
+ 3. Obtiene credenciales VUCEM
+ 4. Obtiene lista de COVE
+ 5. Procesa cada documento para obtener su acuse en PDF
+ 6. Guarda cada PDF procesado
+
+ Args:
+ request: ServiceRemesaSchema con pedimento y organización
+
+ Returns:
+ JSONResponse con lista de COVE procesados
+
+ Raises:
+ HTTPException: En caso de errores de validación o procesamiento
+ """
+ operation_name = "ACUSE_COVES"
+ service_data = None
+
+ try:
+ # Validar datos de entrada
+ request_data = request.model_dump()
+ await _validate_request_data(request_data)
+
+ logger.info(f"Iniciando procesamiento de acuse COVES - Pedimento: {request_data['pedimento']}")
+
+ # Obtener servicio de remesas existente
+ service_data = await _get_pedimento_service(
+ pedimento_id=request_data['pedimento'],
+ service_type=9,
+ operation_name=operation_name
+ )
+
+ # Actualizar estado a "En proceso"
+ update_success = await _update_service_status(
+ service_data['id'], ESTADO_EN_PROCESO, service_data, operation_name
+ )
+ if not update_success:
+ raise HTTPException(status_code=500, detail="Error al actualizar estado del servicio")
+
+ # Obtener credenciales VUCEM
+ contribuyente_id = service_data.get('pedimento', {}).get('contribuyente', '')
+ if not contribuyente_id:
+ logger.error("No se encontró ID de contribuyente en los datos del servicio")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=400, detail="ID de contribuyente no encontrado")
+
+ credentials = await _get_vucem_credentials(contribuyente_id, operation_name)
+
+ # Obtener COVES
+ logger.info("Obteniendo COVES...")
+ try:
+ coves = await rest_controller.get_coves(service_data['pedimento']['id'])
+
+ if not coves:
+ logger.warning("No se encontraron COVES para el pedimento")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=404, detail="No se encontraron COVES para el pedimento")
+
+ logger.info(f"Se encontraron {len(coves)} COVES")
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.error(f"Error al obtener COVES: {e}")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="Error al obtener COVES")
+
+ # Procesar acuses de documentos digitalizados
+ documentos_procesados = []
+ documentos_exitosos = 0
+
+ logger.info(f"Procesando acuses COVE para {len(coves)} documentos...")
+
+ for idx, cove in enumerate(coves):
+ documento_info = {
+ #"clave": cove.get('clave', 'N/A'),
+ #"descripcion": cove.get('descripcion', 'N/A'),
+ "numero_cove": cove.get('numero_cove', 'N/A'),
+ "procesado": False,
+ "error": None
+ }
+
+ # Verificar que el documento tenga número de cove
+ if not cove.get('numero_cove'):
+ logger.warning(f"Documento {idx + 1} no tiene numero_cove, saltando...")
+ documento_info["error"] = "Sin número de cove"
+ documentos_procesados.append(documento_info)
+ continue
+
+ try:
+ logger.info(f"Procesando acuse para documento {idx + 1}: {cove['numero_cove']}")
+
+ soap_response = await get_soap_acuseCOVE(
+ credenciales=credentials,
+ response_service=service_data,
+ soap_controller=soap_controller,
+ cove=cove,
+ idx=idx + 1
+ )
+
+ if soap_response:
+ documento_info["procesado"] = True
+ documento_info["documento"] = soap_response.get('documento', {})
+ documentos_exitosos += 1
+ logger.info(f"Acuse cove del documento {idx + 1} procesado exitosamente")
+ else:
+ documento_info["error"] = "Error en petición SOAP"
+ logger.warning(f"No se pudo procesar el acuse cove del documento {idx + 1}")
+
+ except Exception as e:
+ logger.error(f"Error al procesar acuse cove del documento {idx + 1}: {e}")
+ documento_info["error"] = str(e)
+ # Continuar con los siguientes documentos
+
+ documentos_procesados.append(documento_info)
+
+ # Verificar si se procesó al menos un documento
+ if documentos_exitosos == 0:
+ logger.error("No se pudo procesar ningún acuse cove de documento digitalizado")
+ await _update_service_status(service_data['id'], ESTADO_ERROR, service_data, operation_name)
+ raise HTTPException(status_code=500, detail="No se pudo procesar ningún acuse cove de documento digitalizado")
+
+ # Finalizar servicio exitosamente
+ await _update_service_status(service_data['id'], ESTADO_FINALIZADO, service_data, operation_name)
+
+ # Crear respuesta estandarizada
+ response_data = await _create_response(
+ service_data=service_data,
+ additional_data={
+ "covesDocs": documentos_procesados,
+ "total_documentos": len(coves),
+ "documentos_exitosos": documentos_exitosos,
+ "documentos_fallidos": len(coves) - documentos_exitosos
+ },
+ success_message=f"Se procesaron {documentos_exitosos}/{len(coves)} acuses cove de documentos exitosamente"
+ )
+
+ # Agregar advertencias si hubo documentos fallidos
+ if documentos_exitosos < len(coves):
+ response_data["warnings"] = [
+ f"Se procesaron solo {documentos_exitosos} de {len(coves)} coves"
+ ]
+
+ logger.info(f"Procesamiento de acuses cove completado - Exitosos: {documentos_exitosos}/{len(coves)}")
+ return JSONResponse(content=response_data, status_code=200)
+
+ except HTTPException:
+ # Re-lanzar HTTPExceptions sin modificar
+ raise
+ except Exception as e:
+ 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)}")
+
+
diff --git a/controllers/RESTController.py b/controllers/RESTController.py
new file mode 100644
index 0000000..9868d7e
--- /dev/null
+++ b/controllers/RESTController.py
@@ -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
\ No newline at end of file
diff --git a/controllers/SOAPController.py b/controllers/SOAPController.py
new file mode 100644
index 0000000..20d9b66
--- /dev/null
+++ b/controllers/SOAPController.py
@@ -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'''
+
+
+
+
+ {username}
+ {password}
+
+
+
+
+
+ {numero_operacion}
+
+ {aduana}
+ {patente}
+ {pedimento}
+
+
+
+ '''
+ 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'''
+
+
+
+ {username}
+ {password}
+
+
+
+
+
+
+ {aduana}
+ {patente}
+ {pedimento}
+
+
+
+ '''
+
+ 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'''
+
+
+
+
+ {username}
+ {password}
+
+
+
+
+
+
+ {aduana}
+ {patente}
+ {pedimento}
+ {numero_operacion}
+ {partida}
+
+
+
+
+ '''
+
+ return soap_template
+
+ def generate_acuse_template(self, username: str, password: str, idEDocument: str) -> str:
+ soap_template = f'''
+
+
+
+
+ {username}
+ {password}
+
+
+
+
+
+ {idEDocument}
+
+
+
+ '''
+ 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'''
+
+
+
+
+ {username}
+ {password}
+
+
+
+
+
+ {numero_operacion}
+
+ {aduana}
+ {patente}
+ {pedimento}
+
+
+
+
+ '''
+ 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'''
+
+
+ {username}
+ {password}
+
+
+
+ {idEDocument}
+ 1
+
+
+
+ '''
+ return soap_template
+
+soap_controller = SOAPController() # Instancia global del controlador SOAP
\ No newline at end of file
diff --git a/controllers/XMLController.py b/controllers/XMLController.py
new file mode 100644
index 0000000..87bcfcc
--- /dev/null
+++ b/controllers/XMLController.py
@@ -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ó 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()
\ No newline at end of file
diff --git a/controllers/__init__.py b/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 0000000..d61a255
--- /dev/null
+++ b/core/__init__.py
@@ -0,0 +1 @@
+# Core package
diff --git a/core/config.py b/core/config.py
new file mode 100644
index 0000000..28da31a
--- /dev/null
+++ b/core/config.py
@@ -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()
diff --git a/docs/AUTOMATIC_SERVICES_FLOW.md b/docs/AUTOMATIC_SERVICES_FLOW.md
new file mode 100644
index 0000000..3869bd5
--- /dev/null
+++ b/docs/AUTOMATIC_SERVICES_FLOW.md
@@ -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
diff --git a/docs/REFACTORING_SUMMARY.md b/docs/REFACTORING_SUMMARY.md
new file mode 100644
index 0000000..67a8112
--- /dev/null
+++ b/docs/REFACTORING_SUMMARY.md
@@ -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
diff --git a/docs/routing_options.md b/docs/routing_options.md
new file mode 100644
index 0000000..52724a0
--- /dev/null
+++ b/docs/routing_options.md
@@ -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
diff --git a/examples/fire_and_forget_example.py b/examples/fire_and_forget_example.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/item_service_usage.py b/examples/item_service_usage.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/soap_controller_usage.py b/examples/soap_controller_usage.py
new file mode 100644
index 0000000..e69de29
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..8bce169
--- /dev/null
+++ b/main.py
@@ -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
+ )
\ No newline at end of file
diff --git a/output.pdf b/output.pdf
new file mode 100644
index 0000000..b05fd20
Binary files /dev/null and b/output.pdf differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4fb5cd1
--- /dev/null
+++ b/requirements.txt
@@ -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
diff --git a/schemas/__init__.py b/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/schemas/acuseSchema.py b/schemas/acuseSchema.py
new file mode 100644
index 0000000..fb8b0c0
--- /dev/null
+++ b/schemas/acuseSchema.py
@@ -0,0 +1,8 @@
+from fastapi import FastAPI
+from pydantic import BaseModel
+from uuid import UUID
+
+class AcuseSchema(BaseModel):
+ document_id: str
+
+
diff --git a/schemas/pedimentoSchema.py b/schemas/pedimentoSchema.py
new file mode 100644
index 0000000..9ff6515
--- /dev/null
+++ b/schemas/pedimentoSchema.py
@@ -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": {}
+ }
+ }
\ No newline at end of file
diff --git a/schemas/serviceSchema.py b/schemas/serviceSchema.py
new file mode 100644
index 0000000..4bbacba
--- /dev/null
+++ b/schemas/serviceSchema.py
@@ -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()
\ No newline at end of file
diff --git a/schemas/vucemSchema.py b/schemas/vucemSchema.py
new file mode 100644
index 0000000..3aaa70a
--- /dev/null
+++ b/schemas/vucemSchema.py
@@ -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
+
diff --git a/services/exceptions.py b/services/exceptions.py
new file mode 100644
index 0000000..e69de29
diff --git a/test.xml b/test.xml
new file mode 100644
index 0000000..54335bb
--- /dev/null
+++ b/test.xml
@@ -0,0 +1,439 @@
+
+
+
+
+
+ 2025-07-10T14:24:04Z
+ 2025-07-10T14:25:04Z
+
+
+
+
+
+ false
+ 21277177344
+
+ 2001238
+
+
+ 1
+ Importacion
+
+
+ IN
+ IMPORTACION TEMPORAL DE INSUMOS POR IMMEX
+
+
+ 9
+ INTERIOR DEL PAIS
+
+
+ 230
+ NOGALES, NOGALES, SONORA.
+
+ 20.76130
+ 6745.682
+
+ 7
+ CARRETERO
+
+
+ 7
+ CARRETERO
+
+
+ 7
+ CARRETERO
+
+ GUMM710831HSRZRG08
+ GLG1502247K9
+ 0.00
+ 1642523.00
+ 1642523.00
+
+
+
+ 230
+ NOGALES, NOGALES, SONORA.
+
+
+ 230
+ NOGALES, NOGALES, SONORA.
+
+
+ IN
+ IMPORTACION TEMPORAL DE INSUMOS POR IMMEX
+
+ 2022-07-25-06:00
+ 2009506
+ 1653
+ 2022-07-15-06:00
+
+
+ MTK861014317
+ MAQUILAS TETA KAWI S.A. DE C.V.
+
+ CARRETERA INTERNACIONAL GUADALAJARA-NOGALES
+ KM 1969
+ Empalme
+ 85340
+
+ 0.00
+ 0.00
+ 0.00
+ 0.00
+
+ 230
+ NOGALES, NOGALES, SONORA.
+
+
+ 2022-07-05-06:00
+
+ 1
+ FECHA DE ENTRADA A TERRITORIO NAL.
+
+
+
+ 2022-07-15-06:00
+
+ 2
+ FECHA DE PAGO DE LAS CONTRIBUCIONES
+
+
+ 1412.00
+ 0
+ 1412.00
+
+ MEX
+ MEXICO (ESTADOS UNIDOS MEXICANOS)
+
+
+
+
+ 15
+ PREVALIDAAAA
+
+
+ 2
+ ESPECIFICO
+
+ 240.0000000000
+
+ 0
+ EFECTIVO
+
+ 240.00
+
+
+
+ 23
+ IVA PREV
+
+
+ 1
+ PORCENTUAL
+
+ 16.0000000000
+
+ 0
+ EFECTIVO
+
+ 38.00
+
+
+
+ 1
+ DTA
+
+
+ 4
+ ESPECIFICO (CUOTA FIJA) DTA
+
+ 378.0000000000
+
+ 0
+ EFECTIVO
+
+ 1134.00
+
+
+ 84-401607200
+ LIBRA GUAYMAS LLC
+ 0.000000
+ 0.00
+
+
+ 84-401607200
+ LIBRA GUAYMAS LLC
+ 0.000000
+ 0.00
+
+
+ 84-401607200
+ LIBRA GUAYMAS LLC
+ 0.000000
+ 0.00
+
+
+ COVE2258M9IT4
+
+ FCA
+ FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)
+
+ 0.00
+ 0.000000
+ 84-401607200
+ LIBRA GUAYMAS LLC
+
+
+ COVE2257S9033
+
+ FCA
+ FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)
+
+ 0.00
+ 0.000000
+ 84-401607200
+ LIBRA GUAYMAS LLC
+
+
+ COVE2257PY1Z4
+
+ FCA
+ FRANCO TRANSPORTISTA (... LUGAR DESIGNADO)
+
+ 0.00
+ 0.000000
+ 84-401607200
+ LIBRA GUAYMAS LLC
+
+
+
+
+ PC
+ PEDIMENTO CONSOLIDADO
+
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0170220NCKKN2
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0433220889CP2
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0436220ER86M4
+
+
+
+ SO
+ SOCIO COMERCIAL CERTIFICADO
+
+ AA
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0436220ER86H4
+
+
+
+ CI
+ CERTIFICACION EN MATERIA DE IVA E IEPS
+
+ AAA
+
+
+
+ IM
+ AUTORIZACION DE EMPRESA CON PROGRAMA IMMEX
+
+ 45242006
+
+
+
+ PP
+ PROGRAMAS DE PROMOCIÓN SECTORIAL.
+
+ 20011635
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0170220NG6SJ4
+
+
+
+ RC
+ REMESAS DE CONSOLIDADO
+
+ 1-3
+
+
+
+ ED
+ E_DOCUMENT DOCUMENTO DIGITALIZADO
+
+ 0436220ESLMS1
+
+
+
+ IC
+ IMPORTADOR CERTIFICADO
+
+ O
+
+
+ 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
+ 96
+ 56
+ 7
+ 69
+ 43
+ 95
+ 64
+ 70
+ 8
+ 65
+ 16
+ 79
+ 20
+ 48
+ 26
+ 18
+ 27
+ 54
+ 34
+ 93
+ 44
+ 17
+ 90
+ 37
+ 9
+ 11
+ 51
+ 73
+ 36
+ 66
+ 63
+ 40
+ 88
+ 19
+ 59
+ 15
+ 2
+ 94
+ 68
+ 78
+ 32
+ 39
+ 89
+ 62
+ 4
+ 47
+ 74
+ 41
+ 80
+ 6
+ 42
+ 28
+ 92
+ 49
+ 12
+ 13
+ 84
+ 10
+ 31
+ 87
+ 50
+ 76
+ 22
+ 30
+ 53
+ 71
+ 38
+ 3
+ 33
+ 45
+ 23
+ 25
+ 21
+ 67
+ 85
+ 14
+ 46
+ 29
+ 35
+ 91
+ 24
+ 72
+ 82
+ 61
+ 86
+ 83
+ 77
+ 75
+ 58
+ 81
+ 55
+ 57
+ 5
+ 52
+ 60
+ 1
+
+
+ DTA
+ 1
+
+ 364.00
+
+ 0
+ EFECTIVO
+
+
+
+
+
+
\ No newline at end of file
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/utils/peticiones.py b/utils/peticiones.py
new file mode 100644
index 0000000..11209f2
--- /dev/null
+++ b/utils/peticiones.py
@@ -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 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(' 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 (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 con contenido válido.")
+
+def decode_acuse_base64_content(base64_content): # Testeado
+ """
+ Decodifica el contenido Base64 del acuse y limpia caracteres especiales.
+
+ Args:
+ base64_content (str): Contenido codificado en Base64
+
+ Returns:
+ bytes: Contenido decodificado o None si hay error
+ """
+ try:
+ # Limpiar el contenido Base64 de manera exhaustiva
+ cleaned_content = base64_content
+
+ # Remover entidades HTML/XML como
,
, etc.
+ cleaned_content = re.sub(r'[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 'true' 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)}")
+
+
diff --git a/utils/servicios.py b/utils/servicios.py
new file mode 100644
index 0000000..59e583b
--- /dev/null
+++ b/utils/servicios.py
@@ -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")
\ No newline at end of file