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>
145 lines
4.4 KiB
Python
145 lines
4.4 KiB
Python
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()
|