103 lines
11 KiB
Markdown
103 lines
11 KiB
Markdown
# Casos especiales de integración (Shopify → SAP)
|
|||
|
|
|
||
|
|
Casos que se salen del flujo normal (`docs/flujo_pipeline.md`): cómo se detectan, qué hace el pipeline y **una orden
|
||
|
|
real de ejemplo por tienda**. "sin caso" significa que no se encontró un ejemplo real en esa tienda (no que no pueda ocurrir).
|
||
|
|
Los JSON de Shopify de la mayoría de los ejemplos están en `download/` (`shopify_#<name>.json` = orden,
|
||
|
|
`shopify_transactions_#<name>.json` = transacciones).
|
||
|
|
|
||
|
|
Tiendas: **snobbycl** (órdenes `#NNNN`) y **on-chile-distributor-prod** (órdenes `#CLNNNN`).
|
||
|
|
|
||
|
|
## Regla general de montos
|
||
|
|
|
||
|
|
La **Invoice siempre iguala lo realmente cobrado** en Shopify (suma de las transacciones efectivas).
|
||
|
|
Las líneas se arman en `domain/invoice.py::_lineas_por_cobrado`:
|
||
|
|
|
||
|
|
1. Si el total de las líneas originales (`quantity`) coincide con lo cobrado → se usan esas.
|
||
|
|
2. Si no, y el total de las líneas vigentes (`current_quantity`) coincide → se usan esas.
|
||
|
|
3. Si ninguna coincide → se usan las originales y el control de suma del pago deja la orden en `error`
|
||
|
|
(no se crea el pago).
|
||
|
|
|
||
|
|
Transacciones efectivas (`domain/payment.py::transacciones_efectivas`): solo `status = success` y `kind`
|
||
|
|
`sale` o `capture`. Si una `authorization` tiene su `capture` (`parent_id`), cuenta solo la `capture`.
|
||
|
|
Se ignoran `failure`, `pending`, `refund` y `void`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## A. Pagos
|
||
|
|
|
||
|
|
| # | Caso | Cómo se detecta | Tratamiento | snobbycl | on-chile |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| A1 | Venta normal | una `sale` exitosa = total de la orden | Invoice + pago + ITR (+ ST + Delivery si es tienda) | #8064 | #CL1467 |
|
||
|
|
| A2 | Intentos fallidos o pendientes antes del pago exitoso | varias transacciones, solo una `success`. `payment_gateway_names` lista todos los intentos y **no** indica quién pagó | solo la exitosa suma | #7880 (Transbank `failure` → Fintoc ok), #7903 (Mercado Pago `pending` → Transbank ok), #8063 (4 intentos, uno con status `error`, y 1 éxito) | #CL9958, #CL9994, #CL10041 (36 en septiembre) |
|
||
|
|
| A3 | `authorization` + `capture` (reversso) | dos `success`, la `capture` con `parent_id` = la `authorization` | solo cuenta la `capture`; reversso va como transferencia (`1-1-010-20-000`) | #7902 | #CL9948, #CL9950, #CL9969 (46 en septiembre) |
|
||
|
|
| A4 | Pago dividido real | dos `sale` `success` de gateways distintos | una línea de `PaymentCreditCards` por transacción, cada una con su tarjeta | sin caso | #CL10236 (Fintoc 152.991 + Transbank 54.000), #CL10594, #CL10967; #CL10947 (Transbank + Transbank Webpay, misma tarjeta SAP 4 → dos líneas) |
|
||
|
|
| A5 | Gateway con nombre distinto al configurado | el `gateway` no coincide con la clave de `sap.payment_methods` (comparación exacta) | agregar las variantes al config con la misma tarjeta | sin caso (sus nombres coinciden) | `Venti` (#CL9933, #CL10054) y `Transbank` (#CL10236), equivalentes a `VentiPay` y `Transbank Webpay` |
|
||
|
|
| A6 | Gateway sin configurar | no está en `sap.payment_methods`, o sin `credit_card`, o `type` inválido | `PaymentConfigError`: el documento de pago queda en `error` con el mensaje y **no se crea el pago** (la Invoice ya existe y se retoma después) | prueba sintética con un gateway inexistente | prueba sintética |
|
||
|
|
| A7 | `VoucherNum` | SAP rechaza más de 20 caracteres (`Value too long in property 'VoucherNum'`) | `VoucherNum` = `name` de la orden (máx. 20) | — | #CL1467 (falló con el `payment_id` de 25 caracteres) |
|
||
|
|
|
||
|
|
Datos fijos de las tarjetas (`domain/payment.py`, provisorios): últimos dígitos `1234`, vencimiento `2030-12-31`,
|
||
|
|
`CreditType cr_Regular`; `SplitPayments` y `NumOfPayments` según el config de cada medio.
|
||
|
|
|
||
|
|
## B. Líneas canceladas, devueltas o reembolsadas
|
||
|
|
|
||
|
|
| # | Caso | Cómo se detecta | Tratamiento | snobbycl | on-chile |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| B1 | Línea cancelada **antes** del pago | `refund_line_items` con `restock_type = cancel`; cobrado = total neto (`current_total_price`) | la Invoice usa las líneas vigentes | #7998 (4 unidades, cobrado 339.980; Invoice original DocNum 460 anulada y reintegrada como DocNum 516) | no detectado en septiembre |
|
||
|
|
| B2 | Línea cancelada con dinero reembolsado **después** de cobrar el total | `cancel` + transacción `refund` exitosa; cobrado = total original | la Invoice usa las líneas **originales** (el reembolso es un tratamiento interno de comercial) | sin caso | #CL10072 (cobrado 305.973, reembolsado 71.991) |
|
||
|
|
| B3 | Línea devuelta (`return`) **sin cobro** | `restock_type = return`, sin transacción `refund`, cobrado = total vigente | la Invoice usa las líneas vigentes y `fulfillments_facturados` limita los fulfillments a lo facturado: no se hace ITR, ST ni Delivery de la unidad devuelta (el Delivery fallaría con `KeyError` al no existir la línea base) | sin caso | 15 órdenes, entre ellas #CL10144, #CL10764, #CL10903 |
|
||
|
|
| B4 | Despachada, pagada y **luego reembolsada** (total o parcial) | `financial_status` `refunded` o `partially_refunded`, con fulfillments y sin `cancelled_at` | se integra completa como venta; el reembolso no se refleja en SAP (`ESTADOS_FINANCIEROS_FACTURABLES`) | #7954, #7963, #7978, #7983, #7987, #8020, #8029 (parcial) | #CL9947, #CL9968, #CL9994, #CL9998, #CL10039, #CL10047 (23 en septiembre, aún sin integrar) |
|
||
|
|
| B5 | Reembolso de dinero pendiente, sin líneas | `refund` en `pending` y `refunds` sin `refund_line_items`, total vigente = cobrado | no afecta Invoice ni pago: se integra normal | #8009 | sin caso |
|
||
|
|
| B6 | Pedido de prueba | decisión manual | `status = ignored` (terminal) | #8019 | sin caso |
|
||
|
|
|
||
|
|
## C. Estado de la orden en Shopify
|
||
|
|
|
||
|
|
| # | Caso | Cómo se detecta | Tratamiento | snobbycl | on-chile |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| C1 | Orden **anulada** | `cancelled_at` no vacío, aunque siga `paid` | `status = cancelled` (terminal) con fecha y `cancel_reason` en `last_error`; evento `order_cancelled`; nunca se crean documentos | #7893, #7985, #8003, #7862 (14 en total) | #CL9983, #CL10015, #CL10161 (22 en septiembre) |
|
||
|
|
| C2 | Pago pendiente o no pagada | `financial_status` fuera de `paid`/`partially_refunded`/`refunded` (ej. `pending`, `voided`) | `not_eligible`; se reevalúa en cada ciclo | sin caso vigente (#8010 y #8021 eran `voided` y están anuladas) | #CL10283, #CL10733, #CL10828 (`pending`) |
|
||
|
|
| C3 | Pagada y sin despachar | sin `fulfillments` | `not_eligible` hasta que se despache; al despacharse se integra sola | #8055 | #CL9951, #CL10006 (73 en septiembre) |
|
||
|
|
| C4 | Costo de envío mayor que cero | `total_shipping_price_set > 0` | `error`: el mapeo del envío no está soportado | sin caso | #CL10169, #CL1452, #CL1453 |
|
||
|
|
| C5 | Orden sin líneas vigentes | todas las líneas canceladas | `not_eligible` ("Orden sin líneas vigentes") | sin caso | sin caso |
|
||
|
|
|
||
|
|
Estados terminales: `integrated`, `timeout`, `ignored`, `cancelled` (ver `docs/esquema_base_datos.md`).
|
||
|
|
|
||
|
|
## D. SAP
|
||
|
|
|
||
|
|
| # | Caso | Síntoma | Tratamiento | snobbycl | on-chile |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| D1 | SKU inexistente | `POST Invoices` 400 `Item number is missing` (`-5009`) | crear los artículos (`tools/cargar_items_faltantes.py`, grupo 108); la orden se reintenta | #8064 (primer intento) | detectado antes de integrar: 874 artículos faltantes en las órdenes de septiembre |
|
||
|
|
| D2 | Sin stock en la bodega de origen | el ITR/transferencia es rechazado | entrada de mercancía (`tools/entrada_mercancia_ordenes.py`) | bodegas `8801`, `8802` | bodegas `8803`, `7702` |
|
||
|
|
| D3 | Anular una Invoice | `Cancellation is not possible; posting date exceeds limit of 10 days`. El **DocNum no es el DocEntry** | reintentar cuando SAP lo permita; anular siempre por DocEntry | #7998 (DocNum 460 = DocEntry 514) | — |
|
||
|
|
| D4 | Documento base cerrado | Delivery: `One of the base documents has already been closed` | `error` en cada ciclo hasta resolverlo en SAP | #7863 (antiguo) | — |
|
||
|
|
| D5 | Cliente con RUT | `note_attributes` con un RUT válido | se busca/crea el BusinessPartner `C<RUT>` (crea clientes reales en SAP) | sin caso (ninguna orden trae RUT) | #CL1467 → `C<RUT>` (ej. `C12345678-5`) (977 de 1.049 elegibles traen RUT) |
|
||
|
|
| D6 | Cliente sin RUT | sin RUT válido en `note_attributes` | cliente genérico `sap.generic_card_code` (`C99998`); no se consultan los metafields del customer | #8064 | #CL9948 |
|
||
|
|
|
||
|
|
## E. Datos, cursor y reprocesos
|
||
|
|
|
||
|
|
| # | Caso | Descripción | Tratamiento | snobbycl | on-chile |
|
||
|
|
|---|---|---|---|---|---|
|
||
|
|
| E1 | Cursor `since_id` | se descarga desde `max(id máximo en BD, initial_since_id)`; `initial_since_id` es un **piso**. Órdenes de prueba con ids altos en la BD hacían saltar el cursor | alinear la BD (insertar cabeceras) o borrar las de prueba | — | #CL10809 y #CL10851 saltaban el cursor a fines de septiembre |
|
||
|
|
| E2 | Orden integrada en SAP sin registro local | si se borra su fila y vuelve a descargarse, se integraría de nuevo | reinsertarla como `ignored`; además la idempotencia (E6) adopta los documentos existentes | — | #CL10809 (Invoice 319, pago 67, ITR 23) |
|
||
|
|
| E3 | Timeout | `timeout_days` (60) desde la creación en Shopify | `timeout` (terminal). Corte operativo de on-chile: 01/09/2026 (`initial_since_id` = 8103221690695) | — | órdenes de septiembre en adelante |
|
||
|
|
| E4 | API de Shopify caída | error 401/403/red | el error se propaga: el ciclo registra "Error procesando tienda" y reintenta en el siguiente. Ya no hay fallback con archivos manuales (se retiró; se usó mientras faltaba el scope `read_orders` de on-chile) | — | — |
|
||
|
|
| E5 | Casos para análisis | el pipeline **no** guarda solo los casos especiales | `tools/descargar_ordenes_especiales.py <store>` los baja a `download/` | 26 órdenes | 55 órdenes |
|
||
|
|
| E6 | Documento ya existente en SAP (idempotencia) | la BD local no registra un documento que sí existe en SAP (BD perdida, copiada o editada) | antes de crear Invoice, ITR, StockTransfer o Delivery se busca por `U_ShopifyOrderId` + `U_ShopifyStore` con las mismas líneas y se **adopta**; solo Invoices vigentes (`CancelStatus = csNo`); una Invoice ya pagada no genera otro pago; evento `documento_adoptado`. **Límite:** no hay respaldo para el pago si la Invoice no figura pagada | #8064, #7998 (adopta la Invoice 516, no la anulada 460) | #CL1467 |
|
||
|
|
| E7 | Corte de red | `SSLEOFError`, reset de conexión, timeout | `ShopifyClient` reintenta hasta 4 veces con espera creciente; los errores HTTP no se reintentan | — | corte SSL al paginar cabeceras |
|
||
|
|
|
||
|
|
## F. No cubierto todavía
|
||
|
|
|
||
|
|
- **Reembolso o anulación posterior a integrar:** `integrated` es terminal, por lo que SAP queda con la venta completa y
|
||
|
|
sin nota de crédito ni devolución. Decisión pendiente con contabilidad (`docs/pendientes.md`).
|
||
|
|
- **Fulfillments vs. Invoice:** `fulfillments_facturados` reparte las cantidades de la Invoice entre los fulfillments en
|
||
|
|
orden; si una misma línea se devolviera parcialmente entre varios fulfillments, el reparto sigue ese orden y no el real.
|
||
|
|
- **Transferencias a cuentas distintas:** un solo `TransferAccount` por pago; con dos cuentas el pago da error.
|
||
|
|
|
||
|
|
## Herramientas útiles
|
||
|
|
|
||
|
|
| Herramienta | Uso |
|
||
|
|
|---|---|
|
||
|
|
| `python -m tools.consultar resumen / descargadas / integradas / errores / orden --name "#8064"` | estado de la BD (solo lectura, hora local; `APP_TIMEZONE`) |
|
||
|
|
| `python -m tools.integrar_ordenes <store> <shopify_id> [--apply]` | dry-run o integración de órdenes puntuales |
|
||
|
|
| `python -m tools.probar_pago <store> <shopify_id>` | payload del pago sobre la Invoice real |
|
||
|
|
| `python -m tools.descargar_ordenes_especiales <store>` | guarda casos especiales en `download/` |
|
||
|
|
| `python -m tools.entrada_mercancia_ordenes <config> --warehouse ... --orders ...` | stock para los SKUs de órdenes puntuales |
|