169 lines
7.1 KiB
Markdown
169 lines
7.1 KiB
Markdown
# 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/<store_code>.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.
|