Integración Shopify → SAP Business One (snobbycl y on-chile)
Pipeline de integración de órdenes de Shopify a SAP Service Layer:
Invoice de reserva, pago con tarjeta o transferencia por transacción,
solicitud de traslado, traslado de stock y entrega.
- Invoice igual a lo cobrado; fulfillments limitados a lo facturado
- Estados terminales integrated, timeout, ignored y cancelled
- Idempotencia contra SAP: adopta documentos existentes en vez de duplicar
- Reintentos de red en el cliente de Shopify
- Secretos fuera de los JSON de tienda (${VARIABLE} resuelta desde .env)
- Herramientas de consulta, integración puntual y carga de pruebas
- Documentación en docs/ (flujo, casos especiales, mapeo, configuración)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
901dd8763d
47 files changed
+4709
No files matched your search
@@ -0,0 +1,169 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user