from datetime import datetime import requests import urllib3 import os import json from typing import Optional, Dict, Any from dotenv import load_dotenv from logger_manager import get_logger logger = get_logger() # ============================================================================ urllib3.disable_warnings() load_dotenv(dotenv_path=".env") SAPB1_URL = os.getenv("SAP_B1_URL", "") SAPB1_TIMEOUT = float(os.getenv("SAP_B1_TIMEOUT", 15.0)) SAPB1_VERIFY_SSL = os.getenv("SAP_B1_VERIFY_SSL", "False").lower() == "true" # Almacenamiento global de sesión _sap_session: dict = {"headers": None, "b1session": None, "routeid": None} _sap_credentials: dict = {} # ============================================================================ # LOGIN # ============================================================================ def login() -> dict: if not _sap_credentials: raise RuntimeError("No hay credenciales. Asigna sapb1_manager._sap_credentials antes de llamar login().") payload = { "CompanyDB": _sap_credentials["company"], "UserName": _sap_credentials["username"], "Password": _sap_credentials["password"], } logger.info("🔐 Iniciando sesión en SAP B1 Service Layer...") resp = requests.post( f"{SAPB1_URL}/Login", json=payload, timeout=SAPB1_TIMEOUT, verify=SAPB1_VERIFY_SSL, ) if resp.status_code != 200: raise ConnectionError( f"Login fallido: HTTP {resp.status_code} - {resp.text}" ) data = resp.json() b1session = data["SessionId"] routeid = resp.cookies.get("ROUTEID", "") headers = {"Cookie": f"B1SESSION={b1session}; ROUTEID={routeid}"} _sap_session.update({"headers": headers, "b1session": b1session, "routeid": routeid}) logger.info(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)") return headers # ============================================================================ # LOGOUT # ============================================================================ def logout(): if not _sap_session["b1session"]: logger.warning("⚠️ No hay sesión activa para cerrar.") return logger.info("🔓 Cerrando sesión SAP B1...") try: resp = requests.post( f"{SAPB1_URL}/Logout", headers=_sap_session["headers"], timeout=SAPB1_TIMEOUT, verify=SAPB1_VERIFY_SSL, ) if resp.status_code == 200: logger.info("✅ Sesión SAP B1 cerrada correctamente.") else: logger.warning(f"⚠️ Logout respondió HTTP {resp.status_code}") except Exception as exc: logger.error(f"❌ Error al cerrar sesión: {exc}") finally: _sap_session.update({"headers": None, "b1session": None, "routeid": None}) _sap_credentials.clear() # ============================================================================ # SESIÓN (interna - lazy login) # ============================================================================ def _ensure_session() -> dict: if _sap_session["headers"]: return _sap_session["headers"] if _sap_credentials: return login() raise RuntimeError("No hay sesión ni credenciales para autenticarse en SAP B1.") # ============================================================================ # LOGGING DE RESPUESTA # ============================================================================ def _log_response(method: str, endpoint: str, response: requests.Response, payload: Any = None): log_line = ( f" │ [{method}] {endpoint} → {response.status_code}" f" ({len(response.content)} bytes)" ) if response.status_code >= 400: logger.warning(f" │ ⚠️ {log_line} - {response.text[:300]}") else: logger.info(log_line) # ============================================================================ # PROXY CENTRALIZADO # ============================================================================ def forward_to_sapb1( method: str, endpoint_path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None, ) -> requests.Response: if not method or not endpoint_path: raise ValueError(" │ method y endpoint_path son requeridos") method = method.upper() headers = _ensure_session() url = f"{SAPB1_URL}/{endpoint_path}" req_headers = { "Content-Type": "application/json", "Accept": "application/json", **_sap_session["headers"], } logger.info(f" │ 🔄 [{method}] {endpoint_path} | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") try: resp = requests.request( method=method, url=url, json=json_data, params=params, headers=req_headers, timeout=SAPB1_TIMEOUT, verify=SAPB1_VERIFY_SSL, ) except requests.exceptions.Timeout: raise ConnectionError(f" │ Timeout ({SAPB1_TIMEOUT}s) en {endpoint_path}") except requests.exceptions.ConnectionError as exc: raise ConnectionError(f" │ No se pudo conectar con SAP B1: {exc}") except requests.RequestException as exc: raise ConnectionError(f" │ Error en solicitud HTTP: {exc}") if resp.status_code == 401: if not _sap_credentials: raise ConnectionError("Sesión expirada (401) y no hay credenciales para re-autenticar.") logger.warning(" │ ⚠️ Sesión expirada (401). Reautenticando e intentando una vez...") login() req_headers["Cookie"] = _sap_session["headers"]["Cookie"] resp = requests.request( method=method, url=url, json=json_data, params=params, headers=req_headers, timeout=SAPB1_TIMEOUT, verify=SAPB1_VERIFY_SSL, ) _log_response(method, endpoint_path, resp, json_data) return resp # ============================================================================ # SEND MESSAGE (POST /Messages) # ============================================================================ def send_message(payload: dict) -> dict: resp = forward_to_sapb1("POST", "Messages", json_data=payload) if resp.status_code not in (200, 201): raise ConnectionError( f" │ Error al enviar Mensaje: HTTP {resp.status_code} - {resp.text}" ) result = resp.json() logger.info(f" │ ✅ SAP B1 Service Layer - {resp.status_code}") return result