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:
commit
901dd8763d
47 files changed
+4709
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user