Files

130 lines
5.9 KiB
Markdown
Raw Permalink Normal View History

# Flujo de Proxy SAP
## Función Central: `forward_to_sapb1`
2026-08-17 11:58:38 -05:00
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)` |
2026-08-17 11:58:38 -05:00
## 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.