# Flujo de Proxy SAP ## Función Central: `forward_to_sapb1` Todas las operaciones CRUD pasan por esta función en `core/sap.py`, que actúa como proxy hacia SAP Service Layer. La mayoría de los routers (`business_partners`, `items`, `sales`, `purchases`, `batch`, parte de `catalogs`) no la llaman directamente: pasan por `core/crud_router.py`, una factory que arma el `list/get/create/update` de un recurso OData y delega en `forward_to_sapb1` (ver [Arquitectura](architecture.md)). El proxy hacia el API Gateway de Crystal Reports (`core/api_gateway.py`, usado por `routers/apigateway.py`) sigue el mismo patrón de sesión-global + reintento en 401, mostrado más abajo bajo "Otros proxies". ```mermaid flowchart TD START([Inicio]) --> INPUT["method, endpoint_path,\njson_data, params, headers"] INPUT --> BUILD["Construye URL:\nSAPB1_SL_URL / endpoint_path"] BUILD --> HEADERS["Adjunta headers:\nCookie: B1SESSION=...\nContent-Type: application/json"] HEADERS --> SEND[Envía request a SAP] SEND --> CHECK{Respuesta} CHECK -->|2xx| SUCCESS[Retorna Response al router] CHECK -->|401| REFRESH["Force refresh:\nensure_sap_session()"] CHECK -->|Timeout / Error| FAIL[ConnectionError] REFRESH --> RETRY{Es reintento?} RETRY -->|No| SEND2["Reintenta request\ncon nueva sesión"] RETRY -->|Sí| ERROR["Extrae error.message.value\nde SAP y lanza HTTPException"] SEND2 --> CHECK2{Respuesta} CHECK2 -->|2xx| SUCCESS CHECK2 -->|4xx/5xx| ERROR SUCCESS --> END([Fin]) FAIL --> END ERROR --> END style ERROR fill:#f96,stroke:#a33 ``` ## Diagrama de Secuencia — Operación CRUD Típica Ejemplo con `GET /api/Items`: ```mermaid sequenceDiagram participant F as Frontend participant MW as Middleware participant R as router/items.py participant C as core/sap.py participant SAP as SAP Service Layer F->>MW: GET /api/Items?$skip=0&$top=20\nX-Session-Token: [token] MW->>MW: Valida sesión MW->>R: request.state.emp_id = "123" R->>C: forward_to_sapb1(\n GET, "Items?$skip=0&$top=20&$inlinecount=allpages") C->>SAP: GET /b1s/v1/Items?$skip=0&$top=20\nCookie: B1SESSION=abc123 SAP-->>C: 200 - value: [...], odata.count: 150 C-->>R: Response R-->>F: 200 - data: [...], total: 150 ``` ## Re-autenticación Automática ante 401 Cuando SAP rechaza la sesión actual (401), el sistema renueva automáticamente la sesión de SAP y reintenta: ```mermaid sequenceDiagram participant R as Router participant C as core/sap.py participant SAP as SAP Service Layer R->>C: forward_to_sapb1(GET, "Items?...") C->>SAP: GET /b1s/v1/Items?...\nCookie: B1SESSION=abc123 SAP-->>C: 401 Unauthorized Note over C: Sesión SAP expirada\n→ Re-autentica y reintenta C->>SAP: POST /Login\nUserName: manager, Password: ..., CompanyDB: ... SAP-->>C: 200 SessionId: xyz789 C->>C: Actualiza sap_session_store C->>SAP: GET /b1s/v1/Items?...\nCookie: B1SESSION=xyz789 SAP-->>C: 200 - value: [...] C-->>R: Response 200 Note over R,C: Si el reintento también falla (4xx/5xx)\n→ se extrae error.message.value\n→ se lanza HTTPException ``` ## Parámetros OData y Headers SAP Soportados ### Query Parameters (OData) | Parámetro | Uso | Ejemplo | | -------------- | ---------------------------------- | -------------------------------- | | `$skip` | Paginación — desplazamiento | `$skip=20` | | `$top` | Paginación — límite por página | `$top=20` | | `$select` | Selección de campos | `$select=ItemCode,ItemName` | | `$filter` | Filtrado | `$filter=ItemCode eq 'A001'` | | `$orderby` | Ordenamiento | `$orderby=ItemCode asc` | | `$inlinecount` | Contar registros totales | `$inlinecount=allpages` | ### Headers SAP | Header | Función | | ----------------------------- | ----------------------------------------- | | `B1S-PageSize` | Tamaño de página personalizado | | `B1S-CaseInsensitive` | Búsqueda sin distinción mayúsc/minúscula | | `B1S-ReplaceCollectionsOnPatch` | Reemplazar colecciones completas en PATCH | ## Manejo de Errores | Situación | Comportamiento | | ----------------------- | ---------------------------------------------------------- | | SAP devuelve 2xx | Retorna `Response` al router (200 body, 201 body, 204 vacío) | | SAP devuelve 401 | Re-autentica automáticamente y reintenta 1 vez | | SAP devuelve 4xx/5xx | Extrae `error.message.value` y lanza `HTTPException(status_code, detail)` | | Timeout de conexión | Lanza `ConnectionError` | | Error de red | Lanza `ConnectionError` | | Reintento también falla | Lanza `HTTPException` con el mensaje de error SAP | | SAP no disponible | `HTTPException(500)` | ## Otros proxies (fuera de Service Layer) | Proxy | Módulo | Sesión | Consumido por | | ----- | ------ | ------ | -------------- | | API Gateway (Crystal Reports, `rs/v1/*`) | `core/api_gateway.py` (`forward_to_api_gateway`) | `requests.Session` con cookies, global, se re-loguea en 401 igual que el Service Layer | `routers/apigateway.py` | | HANA (SQL directo) | `core/hana.py` (`hana_connection`) | Sin sesión persistente: abre y cierra una conexión `hdbcli` por request | `routers/reports.py` | `core/hana.py` no pasa por `forward_to_sapb1` ni maneja OData: se usa para reportes que necesitan una consulta SQL puntual contra HANA (por ejemplo, armar el detalle de una guía de remisión para imprimir stickers) en vez de paginar un recurso del Service Layer.