# 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] 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] end subgraph "Routers" A[auth.py\nLogin / Logout] BP[business_partners.py\nSocios de Negocio] IT[items.py\nArtículos] SL[sales.py\nFacturas / Pedidos / Guías] PR[purchases.py\nÓrdenes / Facturas / Guías Compra] CT[catalogs.py\nCatálogos] BT[batch.py\nLotes] PM[permissions.py\nPermisos de Menú] APG[apigateway.py\nCrystal Reports] RP[reports.py\nReportes Jinja/HTML → PDF] end MP[menu_permissions.json] TPL[templates/reports/*.html] end subgraph "External" SAP_SL[SAP B1 Service Layer] SAP_AG[SAP B1 API Gateway\nrs/v1/*] SAP_HANA[(SAP HANA)] end M --> C M --> L C --> L M --> A M --> BP M --> IT M --> SL M --> PR M --> CT M --> BT M --> PM M --> APG M --> RP A --> SAP A --> SES BP --> CRUD IT --> CRUD SL --> CRUD PR --> CRUD CT --> CRUD BT --> CRUD CRUD --> SAP PM --> MP APG --> AG RP --> HANA RP --> TPL SAP --> SAP_SL SES --> SAP AG --> SAP_AG HANA --> SAP_HANA ``` `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. ## 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] ROUTER -->|Otros /api/*| DEST[routers → core/sap.py, core/api_gateway.py o core/hana.py] ``` ### 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 | | `session_middleware` | Valida token de sesión, adjunta el usuario (`webUser`) a `request.state` y al contexto de logging | ## Arquitectura de Sesiones Existen **tres capas de sesión/credenciales independientes**: ```mermaid flowchart LR subgraph Employee["Employee Session"] T["Token aleatorio → webUser"] E["employee_sessions dict"] TK["active_tokens dict"] T --> TK TK --> E end subgraph SAP["SAP B1 Service Layer Session"] S["sap_session_store"] 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 end ``` | 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. ## Estructura de Rutas | Método | Ruta | Router | Descripción | | -------- | --------------------------------- | --------------------- | ------------------------ | | `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. ## 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 main --> apigateway_router[apigateway] main --> reports_router[reports] auth --> sap_core auth --> session_core business_partners --> crud_router items --> crud_router sales --> crud_router purchases --> crud_router catalogs --> crud_router catalogs --> sap_core batch --> crud_router crud_router --> sap_core permissions --> menu_json[menu_permissions.json] apigateway_router --> api_gateway_core[api_gateway] reports_router --> hana_core[hana] reports_router --> templates_reports[templates/reports/*.html] ``` ## Configuración por Entorno 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). 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.