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>
11 KiB
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:
- Si el total de las líneas originales (
quantity) coincide con lo cobrado → se usan esas. - Si no, y el total de las líneas vigentes (
current_quantity) coincide → se usan esas. - 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:
integratedes 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_facturadosreparte 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
TransferAccountpor 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 |