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:
georgeandClaude Sonnet 5.5 committed 2026-09-30 19:22:40 -05:00
commit 901dd8763d
47 files changed
+4709

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
import json
import sys
import time
from datetime import datetime, timedelta
from pathlib import Path
from core.db import init_db
from core.lock import SingleInstanceError, acquire_single_instance_lock
from core.logger_manager import get_logger
from pipeline.runner import ejecutar_ciclo
logger = get_logger()
SCHEDULER_PATH = Path(__file__).resolve().parent / "scheduler.json"
config: dict = {}
# ============================================================================
def cargar_programacion() -> dict:
global config
with open(SCHEDULER_PATH, "r", encoding="utf-8") as f:
data = json.load(f)
data["HORARIOS"] = [tuple(h) for h in data["HORARIOS"]]
config = data
return config
def obtener_horarios() -> list:
return config.get("HORARIOS", [])
def obtener_dias_activos() -> list:
return config.get("DIAS_ACTIVOS", [])
# ============================================================================
def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
minutos = dt.hour * 60 + dt.minute
for hi, mi, hf, mf, intervalo, modo in obtener_horarios():
inicio = hi * 60 + mi
fin = hf * 60 + mf
if inicio <= minutos < fin:
return (intervalo, modo)
return None
# ============================================================================
def segundos_hasta_proximo_inicio(dt: datetime) -> int:
"""
Revisa todos los horarios de hoy (no solo el primero) y toma el próximo
inicio que aún no haya pasado. Si ninguno queda hoy, salta al próximo
día activo.
"""
candidatos = []
for hi, mi, *_ in obtener_horarios():
inicio = dt.replace(hour=hi, minute=mi, second=0, microsecond=0)
if inicio > dt:
candidatos.append(inicio)
if candidatos:
return int((min(candidatos) - dt).total_seconds())
return segundos_hasta_proximo_dia_activo(dt)
# ============================================================================
def segundos_hasta_proximo_dia_activo(dt: datetime) -> int:
horarios = obtener_horarios()
hi, mi = (horarios[0][0], horarios[0][1]) if horarios else (6, 0)
dias_activos = obtener_dias_activos()
for dias in range(1, 8):
prox = dt + timedelta(days=dias)
if prox.weekday() in dias_activos:
target = prox.replace(hour=hi, minute=mi, second=0, microsecond=0)
return int((target - dt).total_seconds())
return 86400
# ============================================================================
def main():
init_db()
cargar_programacion()
logger.info("=" * 60)
logger.info("Servicio Shopify -> SAP iniciado")
logger.info("=" * 60)
while True:
inicio_ciclo = datetime.now()
cargar_programacion()
if inicio_ciclo.weekday() not in obtener_dias_activos():
dormir = segundos_hasta_proximo_dia_activo(inicio_ciclo)
logger.info(
f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Día no activo. "
f"Próxima ejecución: {(inicio_ciclo + timedelta(seconds=dormir)):%Y-%m-%d %H:%M:%S}"
)
time.sleep(dormir)
continue
resultado = obtener_intervalo(inicio_ciclo)
if resultado is not None:
intervalo, modo = resultado
logger.info(f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Iniciando ciclo [{modo}] (cada {intervalo}s)")
ejecutar_ciclo(modo)
duracion = (datetime.now() - inicio_ciclo).total_seconds()
if duracion > intervalo:
logger.warning(f"El ciclo demoró {duracion:.1f}s (intervalo configurado {intervalo}s)")
dormir = max(0, intervalo - duracion)
logger.info(f"Próximo ciclo [{modo}] en {dormir:.0f}s...")
else:
dormir = segundos_hasta_proximo_inicio(inicio_ciclo)
despertar = inicio_ciclo + timedelta(seconds=dormir)
logger.info(
f"[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] Fuera de horario. "
f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}"
)
time.sleep(dormir)
# ============================================================================
if __name__ == "__main__":
try:
lock = acquire_single_instance_lock()
except SingleInstanceError as exc:
logger.error(str(exc))
sys.exit(1)
try:
main()
except KeyboardInterrupt:
logger.info("Servicio detenido por el usuario.")
finally:
lock.release()