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'&#x[0-9a-fA-F]+;', '', cleaned_content) + cleaned_content = re.sub(r'&#[0-9]+;', '', cleaned_content) + + # Remover espacios en blanco, saltos de línea, etc. + cleaned_content = re.sub(r'[\s\n\r\t]', '', cleaned_content) + + # Remover caracteres no válidos para Base64 + cleaned_content = re.sub(r'[^A-Za-z0-9+/=]', '', cleaned_content) + + logger.info(f"Contenido Base64 limpiado: {len(cleaned_content)} caracteres") + + # Agregar padding si es necesario + missing_padding = len(cleaned_content) % 4 + if missing_padding: + cleaned_content += '=' * (4 - missing_padding) + logger.info(f"Padding agregado: {4 - missing_padding} caracteres '='") + + # Decodificar Base64 + decoded_content = base64.b64decode(cleaned_content) + + logger.info(f"Contenido decodificado exitosamente: {len(decoded_content)} bytes") + return decoded_content + + except Exception as e: + logger.error(f"Error decodificando Base64: {e}") + + # Intentar con validación estricta deshabilitada + try: + logger.info("Intentando decodificación con validación relajada...") + decoded_content = base64.b64decode(cleaned_content, validate=False) + logger.info("¡Decodificación exitosa con validación relajada!") + return decoded_content + except Exception as e2: + logger.error(f"Error también con validación relajada: {e2}") + return None + +def soap_error(soap_response): # Testeado + """ + Verifica si la respuesta SOAP no contiene errores. + + Args: + soap_response: Respuesta del servicio SOAP + + Returns: + bool: True si no hay errores, False en caso contrario + """ + if '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