Files
shopify_run_run/domain/business_partner.py
T
georgeandClaude Sonnet 5.5 901dd8763d 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>
2026-09-30 19:22:40 -05:00

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}