# Esquema de base de datos (SQLite) Archivo: `data/shopify_sap.db`. Definido y accedido desde `core/db.py`. Tres tablas: `orders` (cabecera), `order_fulfillments` (un fulfillment de Shopify) y `order_documents` (un documento SAP requerido/creado). ```mermaid erDiagram orders ||--o{ order_fulfillments : "tiene" orders ||--o{ order_documents : "tiene" order_fulfillments ||--o{ order_documents : "tiene (si aplica)" orders { int sys_id PK string store_code int shopify_order_id string order_number string tags string payment_gateway_names string shopify_created_at string time_insert string time_update string financial_status string fulfillment_status string card_code string status string last_error } order_fulfillments { int sys_id PK int order_sys_id FK int shopify_fulfillment_id string location_id bool is_store string warehouse_code string time_insert } order_documents { int sys_id PK int order_sys_id FK int fulfillment_sys_id FK string doc_type int sequence bool required string status int sap_doc_entry int sap_doc_num int base_doc_entry string error_message int attempts string time_update } ``` --- ## `orders` Una fila por orden de Shopify (`UNIQUE(store_code, shopify_order_id)`, así el mismo id de Shopify puede repetirse entre tiendas distintas). | Columna | Tipo | Descripción | |---|---|---| | `sys_id` | INTEGER PK | id interno, autoincremental | | `store_code` | TEXT | qué tienda (`config/stores/.json`) | | `shopify_order_id` | INTEGER | `id` de Shopify | | `order_number` | TEXT | campo `name` de Shopify (ej. `"#7863"`) | | `tags` | TEXT | tal cual viene de Shopify, separado por coma | | `payment_gateway_names` | TEXT | medio(s) de pago, unidos con `" + "` si hay más de uno | | `shopify_created_at` | TEXT | fecha de creación real en Shopify — dispara el `timeout` | | `time_insert` | TEXT | cuándo se descargó/insertó por primera vez (auditoría) | | `time_update` | TEXT | última vez que se tocó esta orden — define el orden de la cola (ver `status`) | | `financial_status` | TEXT | último valor conocido de Shopify (puede cambiar entre corridas) | | `fulfillment_status` | TEXT | ídem — `null`/`partial`/`fulfilled` | | `card_code` | TEXT | `CardCode` de SAP ya resuelto (se cachea, no se vuelve a resolver) | | `status` | TEXT | ver tabla de estados abajo | | `last_error` | TEXT | último mensaje de error (ya diagnosticado, no el crudo de SAP si se reconoció) | ### Valores de `status` | Valor | Terminal | Significado | |---|---|---| | `pending` | No | Cabecera descargada, aún no analizada | | `not_eligible` | No | No está `paid`, o no tiene `fulfillments` todavía | | `in_progress` | No | Tiene al menos un fulfillment procesado, pero `fulfillment_status != "fulfilled"` (pueden llegar más) | | `error` | No | El último intento falló en algún documento; se reintenta en la próxima rotación | | `integrated` | **Sí** | Todos los documentos requeridos están `created` y `fulfillment_status == "fulfilled"` | | `timeout` | **Sí** | Pasaron más de `timeout_days` desde `shopify_created_at` sin llegar a `integrated` | | `cancelled` | **Sí** | La orden tiene `cancelled_at` en Shopify (anulada). El pipeline la marca solo, con la fecha y el `cancel_reason` en `last_error`; nunca crea documentos en SAP | | `ignored` | **Sí** | Marcada a mano para que el pipeline no la procese nunca (ej. pedidos de prueba). No se asigna automáticamente | Solo `integrated`, `timeout`, `ignored` y `cancelled` son terminales — `get_pending_orders` y `mark_orders_timeout` excluyen esos cuatro, todo lo demás se sigue reintentando. --- ## `order_fulfillments` Una fila por `fulfillment` de Shopify (`UNIQUE(order_sys_id, shopify_fulfillment_id)`). Una orden puede tener varios (envío parcial). | Columna | Tipo | Descripción | |---|---|---| | `sys_id` | INTEGER PK | | | `order_sys_id` | FK → `orders.sys_id` | | | `shopify_fulfillment_id` | INTEGER | `id` del fulfillment en Shopify | | `location_id` | TEXT | `location_id` de Shopify de ese fulfillment | | `is_store` | INTEGER (bool) | snapshot de `config.sap.locations[location_id].is_store` al momento de procesarlo | | `warehouse_code` | TEXT | bodega SAP resuelta para ese `location_id` | | `time_insert` | TEXT | cuándo se detectó este fulfillment | --- ## `order_documents` Una fila por documento SAP requerido (exista o no todavía). Documentos a nivel orden (`INVOICE`, `INCOMING_PAYMENT`) tienen `fulfillment_sys_id = NULL`; los de nivel fulfillment (`INVENTORY_TRANSFER_REQUEST`, `STOCK_TRANSFER`, `DELIVERY`) lo tienen seteado. | Columna | Tipo | Descripción | |---|---|---| | `sys_id` | INTEGER PK | | | `order_sys_id` | FK → `orders.sys_id` | | | `fulfillment_sys_id` | FK → `order_fulfillments.sys_id`, nullable | `NULL` para documentos de orden | | `doc_type` | TEXT | `INVOICE`, `INCOMING_PAYMENT`, `INVENTORY_TRANSFER_REQUEST`, `STOCK_TRANSFER`, `DELIVERY` | | `sequence` | INTEGER | orden de creación (10/20/30/40/50 — incrementos de 10 para insertar tipos nuevos sin reordenar) | | `required` | INTEGER (bool) | si este documento aplica para esta orden/fulfillment | | `status` | TEXT | `pending` / `created` / `error` | | `sap_doc_entry` / `sap_doc_num` | INTEGER | identificadores reales en SAP, una vez creado | | `base_doc_entry` | INTEGER | *(reservado, no usado todavía — el `BaseEntry` real se arma al vuelo en `domain/*` a partir del documento base)* | | `error_message` | TEXT | mensaje ya diagnosticado (ver `domain/documents.py::diagnosticar_error`) | | `attempts` | INTEGER | cuántas veces falló | | `time_update` | TEXT | última vez que se tocó esta fila | ### Valores de `status` | Valor | Significado | |---|---| | `pending` | Fila creada (el documento es requerido), todavía no se intentó | | `created` | Ya existe en SAP (`sap_doc_entry`/`sap_doc_num` poblados) — en un reintento, se relee con `GET` en vez de volver a crear | | `error` | Falló el último intento — se reintenta en la próxima corrida | --- ## Funciones de `core/db.py` por tabla **`orders`**: `insert_order_header`, `get_last_shopify_order_id`, `get_order_sys_id`, `mark_orders_timeout`, `get_pending_orders`, `set_order_status`, `set_order_card_code`, `update_order_snapshot`. **`order_fulfillments`**: `ensure_order_fulfillment` (insert-si-no-existe, por `shopify_fulfillment_id`). **`order_documents`**: `ensure_order_document` (insert-si-no-existe, por `doc_type` + `fulfillment_sys_id`), `get_order_document`, `mark_document_created`, `mark_document_error`. --- ## Nota: precisión de `time_update` Todos los `UPDATE`/`DEFAULT` de timestamp usan `strftime('%Y-%m-%d %H:%M:%f','now')` (milisegundos), no `datetime('now')` (que solo tiene precisión de segundo). Esto es necesario para que la rotación justa de `get_pending_orders` (`ORDER BY time_update ASC`) funcione bien cuando el pipeline toca varias órdenes rápido dentro del mismo segundo.