200 lines
7.5 KiB
Python
200 lines
7.5 KiB
Python
|
|||
|
|
from dotenv import load_dotenv
|
||
|
|
import logging
|
||
|
|
import requests
|
||
|
|
from typing import Optional, Dict, Any
|
||
|
|
import os
|
||
|
|
from fastapi import FastAPI, HTTPException, Request
|
||
|
|
import json
|
||
|
|
import urllib3
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# CONFIGURACIÓN
|
||
|
|
# ============================================================================
|
||
|
|
|
||
|
|
# Cargar variables de entorno desde .env
|
||
|
|
load_dotenv(dotenv_path=".env")
|
||
|
|
|
||
|
|
SAPB1_SL_URL = os.getenv("SAPB1_SL_URL", "")
|
||
|
|
SAPB1_SL_USER = os.getenv("SAPB1_SL_USER", "")
|
||
|
|
SAPB1_SL_PASSWORD = os.getenv("SAPB1_SL_PASSWORD", "")
|
||
|
|
SAPB1_SL_COMPANYDB = os.getenv("SAPB1_SL_COMPANYDB", "")
|
||
|
|
# Tiempo de espera y verificación SSL
|
||
|
|
SAPB1_SL_TIMEOUT = float(os.getenv("SAPB1_SL_TIMEOUT", 15.0))
|
||
|
|
SAPB1_SL_VERIFY_SSL = os.getenv("SAPB1_SL_VERIFY_SSL", "False").lower() == "true"
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Obtiene el logger específico de este archivo
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# 1. Diccionario global para persistir las cabeceras de autenticación de SAP
|
||
|
|
sap_session_store: dict[str, Optional[dict[str, str]]] = {
|
||
|
|
"headers": None
|
||
|
|
}
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
def _log_response(
|
||
|
|
method: str,
|
||
|
|
endpoint: str,
|
||
|
|
response: requests.Response,
|
||
|
|
request_data: Optional[Dict] = None
|
||
|
|
) -> None:
|
||
|
|
"""Registra detalles de la respuesta"""
|
||
|
|
extra = {"http_method": method, "endpoint": endpoint, "status_code": response.status_code}
|
||
|
|
if response.status_code >= 400:
|
||
|
|
try:
|
||
|
|
error_detail = response.json()
|
||
|
|
respuesta = json.dumps(error_detail, indent=2)[:500]
|
||
|
|
except Exception:
|
||
|
|
respuesta = response.text[:500]
|
||
|
|
logger.error(
|
||
|
|
f"[{response.status_code}] {method} {endpoint} - "
|
||
|
|
f"Datos enviados: {json.dumps(request_data, indent=2, default=str)[:500]} - "
|
||
|
|
f"Respuesta: {respuesta}",
|
||
|
|
extra=extra
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
logger.info(f"[{response.status_code}] {method} {endpoint}", extra=extra)
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
def ensure_sap_session(force_refresh: bool = False) -> dict:
|
||
|
|
"""
|
||
|
|
Verifica si existe una sesión global de SAP B1 (B1SESSION).
|
||
|
|
Si no existe o si force_refresh es True, realiza el Login con la información del .env.
|
||
|
|
"""
|
||
|
|
if sap_session_store["headers"] and not force_refresh:
|
||
|
|
return sap_session_store["headers"]
|
||
|
|
|
||
|
|
logger.info("Iniciando sesión global en SAP como Manager...")
|
||
|
|
auth_url = f"{SAPB1_SL_URL}/Login"
|
||
|
|
sap_auth_payload = {
|
||
|
|
"CompanyDB": SAPB1_SL_COMPANYDB,
|
||
|
|
"UserName": SAPB1_SL_USER,
|
||
|
|
"Password": SAPB1_SL_PASSWORD
|
||
|
|
}
|
||
|
|
|
||
|
|
try:
|
||
|
|
response = requests.post(
|
||
|
|
auth_url,
|
||
|
|
json=sap_auth_payload,
|
||
|
|
timeout=SAPB1_SL_TIMEOUT,
|
||
|
|
verify=SAPB1_SL_VERIFY_SSL
|
||
|
|
)
|
||
|
|
|
||
|
|
if response.status_code != 200:
|
||
|
|
logger.error(f"Error crítico: No se pudo conectar el usuario Manager a SAP. Status: {response.status_code}")
|
||
|
|
sap_session_store["headers"] = None
|
||
|
|
raise HTTPException(status_code=response.status_code, detail="Conexión fallida con el servidor SAP")
|
||
|
|
|
||
|
|
sap_session = response.json()
|
||
|
|
b1_session_id = sap_session.get("SessionId")
|
||
|
|
route_id = response.cookies.get("ROUTEID")
|
||
|
|
|
||
|
|
headers = {
|
||
|
|
"Cookie": f"B1SESSION={b1_session_id}; ROUTEID={route_id}"
|
||
|
|
}
|
||
|
|
sap_session_store["headers"] = headers
|
||
|
|
logger.info(f"Sesión global establecida. B1SESSION: {b1_session_id[:10]}...")
|
||
|
|
return headers
|
||
|
|
except requests.RequestException as exc:
|
||
|
|
logger.error(f"Error de conexión al autenticar contra SAP: {exc}")
|
||
|
|
sap_session_store["headers"] = None
|
||
|
|
raise HTTPException(status_code=500, detail=f"Error al conectar con SAP Service Layer: {exc}")
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# FUNCIÓN PROXY CENTRALIZADA
|
||
|
|
# ============================================================================
|
||
|
|
def forward_to_sapb1(
|
||
|
|
method: str,
|
||
|
|
endpoint_path: str,
|
||
|
|
json_data: Optional[Dict[str, Any]] = None,
|
||
|
|
params: Optional[Dict[str, Any]] = None,
|
||
|
|
extra_headers: Optional[Dict[str, str]] = None
|
||
|
|
) -> requests.Response:
|
||
|
|
"""
|
||
|
|
Función proxy centralizada para todas las llamadas a SAP B1 Service Layer.
|
||
|
|
Maneja autenticación transparentemente con sesión única y reintento.
|
||
|
|
"""
|
||
|
|
if not method or not endpoint_path:
|
||
|
|
raise ValueError("method y endpoint_path son requeridos")
|
||
|
|
|
||
|
|
method = method.upper()
|
||
|
|
|
||
|
|
# Obtener cabeceras con B1SESSION
|
||
|
|
headers = ensure_sap_session()
|
||
|
|
|
||
|
|
# Preparar URL
|
||
|
|
url_destino = f"{SAPB1_SL_URL}/{endpoint_path}"
|
||
|
|
|
||
|
|
# Preparar cabeceras de la petición
|
||
|
|
req_headers = {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
'Accept': 'application/json'
|
||
|
|
}
|
||
|
|
req_headers.update(headers)
|
||
|
|
if extra_headers:
|
||
|
|
req_headers.update(extra_headers)
|
||
|
|
|
||
|
|
log_extra = {"http_method": method, "endpoint": endpoint_path}
|
||
|
|
logger.info(f"[{method}] {endpoint_path}", extra=log_extra)
|
||
|
|
|
||
|
|
# Realizar solicitud
|
||
|
|
try:
|
||
|
|
response = requests.request(
|
||
|
|
method=method,
|
||
|
|
url=url_destino,
|
||
|
|
json=json_data,
|
||
|
|
params=params,
|
||
|
|
headers=req_headers,
|
||
|
|
timeout=SAPB1_SL_TIMEOUT,
|
||
|
|
verify=SAPB1_SL_VERIFY_SSL
|
||
|
|
)
|
||
|
|
except requests.exceptions.Timeout:
|
||
|
|
logger.error(f"Timeout ({SAPB1_SL_TIMEOUT}s) en {endpoint_path}", extra=log_extra)
|
||
|
|
raise ConnectionError(f"Timeout al conectar con {endpoint_path}")
|
||
|
|
except requests.exceptions.ConnectionError as exc:
|
||
|
|
logger.error(f"Error de conexión: {exc}", extra=log_extra)
|
||
|
|
raise ConnectionError(f"No se pudo conectar con SAP B1: {exc}")
|
||
|
|
except requests.RequestException as exc:
|
||
|
|
logger.error(f"Error en solicitud: {exc}", extra=log_extra)
|
||
|
|
raise ConnectionError(f"Error en solicitud HTTP: {exc}")
|
||
|
|
|
||
|
|
# Manejo de 401 - Sesión expirada: reintentar UNA sola vez
|
||
|
|
if response.status_code == 401:
|
||
|
|
logger.warning("Sesión expirada (401). Reautenticando e intentando una sola vez...", extra=log_extra)
|
||
|
|
try:
|
||
|
|
# Forzar actualización de sesión
|
||
|
|
headers = ensure_sap_session(force_refresh=True)
|
||
|
|
req_headers.update(headers)
|
||
|
|
|
||
|
|
logger.info("Reintentando solicitud [RETRY]", extra=log_extra)
|
||
|
|
response = requests.request(
|
||
|
|
method=method,
|
||
|
|
url=url_destino,
|
||
|
|
json=json_data,
|
||
|
|
params=params,
|
||
|
|
headers=req_headers,
|
||
|
|
timeout=SAPB1_SL_TIMEOUT,
|
||
|
|
verify=SAPB1_SL_VERIFY_SSL
|
||
|
|
)
|
||
|
|
except Exception as exc:
|
||
|
|
logger.error(f"Error en reintento: {exc}", extra=log_extra)
|
||
|
|
raise ConnectionError(f"Error en reintento tras reautenticación: {exc}")
|
||
|
|
|
||
|
|
# Logging de respuesta
|
||
|
|
_log_response(method, endpoint_path, response, json_data)
|
||
|
|
|
||
|
|
# Extraer error SAP y propagar al frontend
|
||
|
|
if response.status_code >= 400:
|
||
|
|
error_msg = response.text
|
||
|
|
try:
|
||
|
|
err = response.json()
|
||
|
|
error_msg = err.get("error", {}).get("message", {}).get("value", error_msg)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
raise HTTPException(status_code=response.status_code, detail=error_msg)
|
||
|
|
|
||
|
|
return response
|
||
|
|
# ============================================================================
|