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>
118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
import re
|
|
from typing import Any, Dict, Optional
|
|
|
|
from core.errors import SAPServiceLayerError
|
|
from core.sap import SAPServiceLayerClient
|
|
from domain.rut import formatear_rut, normalizar_rut, validar_rut
|
|
|
|
RUT_PATTERN = re.compile(r"^\d{1,2}\.?\d{3}\.?\d{3}-?[\dkK]$")
|
|
BUSINESS_PARTNER_GROUP_CODE = 100 # "Clientes", mismo grupo que usa el CardCode genérico
|
|
|
|
|
|
def _nombre_completo(persona: Dict[str, Any]) -> str:
|
|
nombre = f"{persona.get('first_name', '') or ''} {persona.get('last_name', '') or ''}".strip()
|
|
return nombre or persona.get("name", "") or ""
|
|
|
|
|
|
def buscar_rut_en_order(order: Dict[str, Any]) -> Optional[str]:
|
|
"""
|
|
Busca el RUT en note_attributes de la orden, donde algunas tiendas lo
|
|
capturan en el checkout (ej. {"name": "rut", "value": "11111109-K"}).
|
|
No requiere llamada extra a Shopify: ya viene en el detalle de la orden.
|
|
"""
|
|
for attr in order.get("note_attributes") or []:
|
|
nombre = str(attr.get("name", "")).strip().lower()
|
|
if "rut" not in nombre:
|
|
continue
|
|
valor = str(attr.get("value", "")).strip()
|
|
if RUT_PATTERN.match(valor) and validar_rut(valor):
|
|
return normalizar_rut(valor)
|
|
|
|
return None
|
|
|
|
|
|
def _bp_address(tipo: str, nombre: str, direccion: Dict[str, Any]) -> Dict[str, Any]:
|
|
address = {
|
|
"AddressName": nombre,
|
|
"AddressType": tipo,
|
|
"Street": direccion.get("address1"),
|
|
"Block": direccion.get("address2"),
|
|
"City": direccion.get("city"),
|
|
"ZipCode": direccion.get("zip"),
|
|
"County": direccion.get("province"),
|
|
}
|
|
if direccion.get("country_code") == "CL":
|
|
address["Country"] = "CL"
|
|
if tipo == "bo_ShipTo":
|
|
address["TaxCode"] = "IVA"
|
|
return address
|
|
|
|
|
|
def crear_business_partner(sap: SAPServiceLayerClient, rut_sap: str, order: Dict[str, Any]) -> str:
|
|
"""
|
|
Crea un BusinessPartner (cliente) nuevo para este RUT. CardCode = "C" +
|
|
rut_sap. Devuelve el CardCode creado.
|
|
"""
|
|
billing = order.get("billing_address") or {}
|
|
shipping = order.get("shipping_address") or {}
|
|
customer = order.get("customer") or {}
|
|
card_code = f"C{rut_sap}"
|
|
|
|
payload = {
|
|
"CardCode": card_code,
|
|
"CardName": _nombre_completo(billing) or _nombre_completo(customer) or "Cliente Shopify",
|
|
"CardType": "cCustomer",
|
|
"GroupCode": BUSINESS_PARTNER_GROUP_CODE,
|
|
"FederalTaxID": rut_sap,
|
|
"Phone1": billing.get("phone") or customer.get("phone"),
|
|
"EmailAddress": order.get("email") or order.get("contact_email"),
|
|
"BPAddresses": [
|
|
_bp_address("bo_BillTo", "BillTo", billing),
|
|
_bp_address("bo_ShipTo", "ShipTo", shipping or billing),
|
|
],
|
|
}
|
|
|
|
sap.forward("POST", "BusinessPartners", json_data=payload)
|
|
return card_code
|
|
|
|
|
|
def resolver_card_code(
|
|
sap: SAPServiceLayerClient,
|
|
store_config: Dict[str, Any],
|
|
rut: Optional[str],
|
|
order: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Si hay un RUT válido, busca el BusinessPartner en SAP por FederalTaxID
|
|
(con guión, que es el formato que usa SAP: "NNNNNNNN-D"). Si existe,
|
|
devuelve su CardCode. Si no existe, crea uno nuevo (CardCode "C"+rut).
|
|
Si no hay RUT, o falla la creación, usa el CardCode genérico
|
|
configurado para la tienda.
|
|
|
|
Siempre devuelve "rut_sap" (el RUT en formato SAP) cuando hay RUT, para
|
|
poder sobrescribir el FederalTaxID del documento aunque se use el
|
|
CardCode genérico.
|
|
"""
|
|
generic_card_code = store_config["sap"]["generic_card_code"]
|
|
|
|
if not rut:
|
|
return {"card_code": generic_card_code, "rut": None, "rut_sap": None, "encontrado_por_rut": False}
|
|
|
|
rut_sap = formatear_rut(rut)
|
|
|
|
resp = sap.forward(
|
|
"GET",
|
|
"BusinessPartners",
|
|
params={"$select": "CardCode", "$filter": f"FederalTaxID eq '{rut_sap}'"},
|
|
)
|
|
resultados = resp.json().get("value", [])
|
|
|
|
if resultados:
|
|
return {"card_code": resultados[0]["CardCode"], "rut": rut, "rut_sap": rut_sap, "encontrado_por_rut": True}
|
|
|
|
try:
|
|
card_code = crear_business_partner(sap, rut_sap, order or {})
|
|
return {"card_code": card_code, "rut": rut, "rut_sap": rut_sap, "encontrado_por_rut": True}
|
|
except SAPServiceLayerError:
|
|
return {"card_code": generic_card_code, "rut": rut, "rut_sap": rut_sap, "encontrado_por_rut": False}
|