2026-08-17 10:34:12 -05:00
# Arquitectura General
## Diagrama de Componentes
```mermaid
graph TB
subgraph "FastAPI Application"
M[main.py\nEntry Point]
C[context.py\nRequest ID Context]
L[logger_config.py\nStructured Logging]
subgraph "Core"
SAP[core/sap.py\nSAP Session & Proxy]
SES[core/session.py\nEmployee Sessions]
2026-08-17 11:58:38 -05:00
HANA[core/hana.py\nConexión directa HANA]
AG[core/api_gateway.py\nSesión y Proxy APIGateway]
CRUD[core/crud_router.py\nFactory de routers CRUD OData]
2026-08-17 10:34:12 -05:00
end
subgraph "Routers"
A[auth.py\nLogin / Logout]
BP[business_partners.py\nSocios de Negocio]
IT[items.py\nArtículos]
2026-08-17 11:58:38 -05:00
SL[sales.py\nFacturas / Pedidos / Guías]
PR[purchases.py\nÓrdenes / Facturas / Guías Compra]
2026-08-17 10:34:12 -05:00
CT[catalogs.py\nCatálogos]
BT[batch.py\nLotes]
PM[permissions.py\nPermisos de Menú]
2026-08-17 11:58:38 -05:00
APG[apigateway.py\nCrystal Reports]
RP[reports.py\nReportes Jinja/HTML → PDF]
2026-08-17 10:34:12 -05:00
end
MP[menu_permissions.json]
2026-08-17 11:58:38 -05:00
TPL[templates/reports/*.html]
2026-08-17 10:34:12 -05:00
end
subgraph "External"
SAP_SL[SAP B1 Service Layer]
2026-08-17 11:58:38 -05:00
SAP_AG[SAP B1 API Gateway\nrs/v1/*]
SAP_HANA[(SAP HANA)]
2026-08-17 10:34:12 -05:00
end
M --> C
M --> L
C --> L
M --> A
M --> BP
M --> IT
M --> SL
M --> PR
M --> CT
M --> BT
M --> PM
2026-08-17 11:58:38 -05:00
M --> APG
M --> RP
2026-08-17 10:34:12 -05:00
A --> SAP
A --> SES
2026-08-17 11:58:38 -05:00
BP --> CRUD
IT --> CRUD
SL --> CRUD
PR --> CRUD
CT --> CRUD
BT --> CRUD
CRUD --> SAP
2026-08-17 10:34:12 -05:00
PM --> MP
2026-08-17 11:58:38 -05:00
APG --> AG
RP --> HANA
RP --> TPL
2026-08-17 10:34:12 -05:00
SAP --> SAP_SL
SES --> SAP
2026-08-17 11:58:38 -05:00
AG --> SAP_AG
HANA --> SAP_HANA
2026-08-17 10:34:12 -05:00
```
2026-08-17 11:58:38 -05:00
`business_partners.py` , `items.py` , `sales.py` , `purchases.py` , `batch.py` y
la mayor parte de `catalogs.py` ya no implementan cada endpoint a mano: usan
`core/crud_router.py` , una factory (`make_crud_router` ) que genera el
`list/get/create/update` típico de un recurso OData (mismo bloque de
`$skip` /`$top` /`$filter` y llamada a `forward_to_sapb1` ) a partir de un
nombre de recurso y unos flags (si soporta `$orderby` , si expone `POST` ,
headers extra para el `PATCH` , etc). Esto evita que un copy-paste entre
recursos deje, por ejemplo, un `PATCH` apuntando al endpoint de otro
recurso. Un router de documentos de mercadeo (ventas o compras) puede
generar varios recursos a la vez iterando esa factory, por eso `sales.py` y
`purchases.py` ya no son un único router sino un `APIRouter` que incluye uno
por cada tipo de documento.
2026-08-17 10:34:12 -05:00
## Pipeline de Middleware
Las peticiones atraviesan los siguientes middleware en orden:
```mermaid
flowchart LR
REQ[Request] --> CORS[CORSMiddleware\nallow_origins=*]
CORS --> CID[correlation_id_middleware\nX-Request-ID]
CID --> SES_MW[session_middleware\nValida X-Session-Token]
SES_MW --> ROUTER{Router}
ROUTER -->|/api/Login| A[auth.py]
2026-08-17 11:58:38 -05:00
ROUTER -->|Otros /api/*| DEST[routers → core/sap.py, core/api_gateway.py o core/hana.py]
2026-08-17 10:34:12 -05:00
```
### Detalle de Middleware
| Middleware | Función |
| -------------------------- | ----------------------------------------------------------- |
| `CORSMiddleware` | Permite peticiones desde cualquier origen |
| `correlation_id_middleware` | Genera o extrae `X-Request-ID` para trazabilidad en logs |
2026-08-17 11:58:38 -05:00
| `session_middleware` | Valida token de sesión, adjunta el usuario (`webUser` ) a `request.state` y al contexto de logging |
2026-08-17 10:34:12 -05:00
## Arquitectura de Sesiones
2026-08-17 11:58:38 -05:00
Existen **tres capas de sesión/credenciales independientes** :
2026-08-17 10:34:12 -05:00
```mermaid
flowchart LR
subgraph Employee["Employee Session"]
2026-08-17 11:58:38 -05:00
T["Token aleatorio → webUser"]
2026-08-17 10:34:12 -05:00
E["employee_sessions dict"]
TK["active_tokens dict"]
T --> TK
TK --> E
end
2026-08-17 11:58:38 -05:00
subgraph SAP["SAP B1 Service Layer Session"]
2026-08-17 10:34:12 -05:00
S["sap_session_store"]
2026-08-17 11:58:38 -05:00
MC["Manager Credentials (.env: SAPB1_SL_*)"]
S -.->|Renueva en 401| MC
end
subgraph AG["SAP API Gateway Session"]
AS["api_gateway_session (requests.Session)"]
AC["Manager Credentials (.env: APIGATEWAY_*)"]
AS -.->|Renueva en 401| AC
2026-08-17 10:34:12 -05:00
end
```
2026-08-17 11:58:38 -05:00
| Capa | Almacenamiento | Ámbito | Timeout | Renovación |
| -------------- | -------------------- | ------------ | ------------------- | --------------------------- |
| SAP Service Layer | `sap_session_store` (`core/sap.py` ) | Global | Gestionado SAP | Automática al recibir 401 |
| SAP API Gateway | `api_gateway_session` (`core/api_gateway.py` ) | Global | Gestionado SAP | Automática al recibir 401 |
| Empleado | `active_tokens` / `employee_sessions` (`core/session.py` ) | Por empleado | 30 min inactividad | Login manual |
`core/hana.py` no mantiene sesión: abre una conexión nueva a HANA por
petición (`hana_connection()` , context manager) y la cierra al terminar.
2026-08-17 10:34:12 -05:00
## Estructura de Rutas
| Método | Ruta | Router | Descripción |
2026-08-17 11:58:38 -05:00
| -------- | --------------------------------- | --------------------- | ------------------------ |
| `POST` | `/api/Login` | `auth.py` | Autenticar empleado |
| `POST` | `/api/Logout` | `auth.py` | Cerrar sesión |
| `GET` | `/api/BusinessPartners` | `business_partners` | Listar socios |
| `GET` | `/api/BusinessPartners/{code}` | `business_partners` | Obtener socio |
| `POST` | `/api/BusinessPartners` | `business_partners` | Crear socio |
| `PATCH` | `/api/BusinessPartners/{code}` | `business_partners` | Actualizar socio |
| `GET` | `/api/Items` | `items.py` | Listar artículos |
| `GET` | `/api/Items/{code}` | `items.py` | Obtener artículo |
| `POST` | `/api/Items` | `items.py` | Crear artículo |
| `PATCH` | `/api/Items/{code}` | `items.py` | Actualizar artículo |
| `GET/POST/PATCH` | `/api/Invoices[/{key}]` | `sales.py` | Facturas de venta |
| `GET/POST/PATCH` | `/api/Orders[/{key}]` | `sales.py` | Pedidos de venta |
| `GET/POST/PATCH` | `/api/DeliveryNotes[/{key}]` | `sales.py` | Guías de remisión de venta |
| `GET/PATCH` | `/api/PurchaseOrders[/{key}]` | `purchases.py` | Órdenes de compra |
| `GET/PATCH` | `/api/PurchaseDeliveryNotes[/{key}]` | `purchases.py` | Guías de recepción de compra |
| `GET/PATCH` | `/api/PurchaseInvoices[/{key}]` | `purchases.py` | Facturas de compra |
| `GET` | `/api/Currencies` | `catalogs.py` | Listar monedas |
| `GET` | `/api/BusinessPartnerGroups` | `catalogs.py` | Listar grupos de socios |
| `GET` | `/api/ItemGroups` | `catalogs.py` | Listar grupos artículos |
| `GET` | `/api/Warehouses` | `catalogs.py` | Listar almacenes |
| `GET` | `/api/SalesTaxCodes` | `catalogs.py` | Listar códigos de IVA |
| `GET` | `/api/FactoringIndicators` | `catalogs.py` | Listar indicadores de factoring |
| `GET` | `/api/Sunat/BusinessPartners/DocumentType` | `catalogs.py` | Tipos de documento SUNAT (campo de usuario `EXX_TIPODOCU` ) |
| `GET` | `/api/BatchNumberDetails` | `batch.py` | Listar lotes |
| `GET` | `/api/BatchNumberDetails/{id}` | `batch.py` | Obtener lote |
| `PATCH` | `/api/BatchNumberDetails/{id}` | `batch.py` | Actualizar lote |
| `GET` | `/api/user/menu` | `permissions.py` | Obtener menú permitido |
| `GET` | `/api/apigateway/reports` | `apigateway.py` | Listar reportes Crystal Reports autorizados |
| `GET` | `/api/apigateway/reports/{docCode}/parameters` | `apigateway.py` | Parámetros de un reporte Crystal Reports |
| `POST` | `/api/apigateway/reports/{docCode}/export` | `apigateway.py` | Exportar reporte Crystal Reports a PDF |
| `POST` | `/api/reports/stickers` | `reports.py` | Generar PDF/HTML de stickers de guía de remisión (consulta directa a HANA + plantilla Jinja + WeasyPrint) |
Los recursos servidos por `core/crud_router.py` (todos los de la tabla salvo
`auth` , `permissions` , `apigateway` y `reports` ) soportan además `skip` ,
`top` , `fields` y `filter` como query params, y algunos (`Invoices` ,
`Orders` , `DeliveryNotes` , `PurchaseOrders` , `PurchaseDeliveryNotes` ,
`PurchaseInvoices` , `BatchNumberDetails` ) también `orderby` . Ver
[API Reference ](api-reference.md ) para el detalle completo por endpoint.
2026-08-17 10:34:12 -05:00
## Dependencias del Proyecto
```mermaid
graph TD
main --> context
main --> logger_config
logger_config --> context
main --> auth
main --> business_partners
main --> items
main --> sales
main --> purchases
main --> catalogs
main --> batch
main --> permissions
2026-08-17 11:58:38 -05:00
main --> apigateway_router[apigateway]
main --> reports_router[reports]
2026-08-17 10:34:12 -05:00
auth --> sap_core
auth --> session_core
2026-08-17 11:58:38 -05:00
business_partners --> crud_router
items --> crud_router
sales --> crud_router
purchases --> crud_router
catalogs --> crud_router
2026-08-17 10:34:12 -05:00
catalogs --> sap_core
2026-08-17 11:58:38 -05:00
batch --> crud_router
crud_router --> sap_core
2026-08-17 10:34:12 -05:00
permissions --> menu_json[menu_permissions.json]
2026-08-17 11:58:38 -05:00
apigateway_router --> api_gateway_core[api_gateway]
reports_router --> hana_core[hana]
reports_router --> templates_reports[templates/reports/*.html]
2026-08-17 10:34:12 -05:00
```
## Configuración por Entorno
2026-08-17 11:58:38 -05:00
La aplicación carga siempre `mini-backend/.env` (un solo archivo por
despliegue, sin sufijos de entorno) mediante `load_dotenv` . Ese archivo
**no se versiona** (está en `.gitignore` ); la plantilla de referencia con
todas las variables necesarias y valores de ejemplo es
[`mini-backend/.env.example` ](../.env.example ).
2026-08-17 10:34:12 -05:00
2026-08-17 11:58:38 -05:00
Grupos de variables:
| Grupo | Variables | Usado por |
| --------------------- | -------------------------------------------------------------------------- | --------------------- |
| SAP Service Layer | `SAPB1_SL_URL` , `SAPB1_SL_USER` , `SAPB1_SL_PASSWORD` , `SAPB1_SL_COMPANYDB` , `SAPB1_SL_TIMEOUT` , `SAPB1_SL_VERIFY_SSL` | `core/sap.py` , `routers/auth.py` |
| SAP API Gateway | `APIGATEWAY_URL` , `APIGATEWAY_USER` , `APIGATEWAY_PASSWORD` , `APIGATEWAY_COMPANYDB` , `APIGATEWAY_DBINSTANCE` , `APIGATEWAY_TIMEOUT` | `core/api_gateway.py` |
| HANA (acceso directo) | `HANA_HOST` , `HANA_PORT` , `HANA_USER` , `HANA_PASSWORD` , `HANA_SCHEMA` | `core/hana.py` , `routers/reports.py` |
| Logging | `LOG_ENVIRONMENT` (`production` = JSON estructurado, cualquier otro valor = texto legible en consola) | `main.py` , `logger_config.py` |
Al desplegar para un cliente nuevo (ver flujo de branches por cliente en
`CLAUDE.md` de la raíz), se copia `.env.example` a `.env` dentro de
`mini-backend/` y se completa con las credenciales reales de SAP Business
One / HANA de ese cliente. Las credenciales reales nunca deben
documentarse ni commitearse.