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 # ============================================================================