#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Fase 2 — MARCA → SUCURSAL V2: match directo por id_contacto_sucursal. Insertar entre `Datos API Cuenta objetivo - SUCURSAL` y `Buscar Contacto Objetivo - SUCURSAL(mail)`: 1) Code `Extraer id_contacto_sucursal de Marca` — saca CF del contacto Marca. 2) IF `¿cfValue presente?` — si vacío salta directo a cascada email. 3) HTTP GET `Match directo por id_contacto_sucursal` — GET /contacts/{cfValue} con token sucursal. onError: continueRegularOutput. 4) IF `¿GET de contacto sucursal OK?` — true→Conseguir Custom...SUCURSAL (UPDATE), false→cascada email. Uso: python n8n/_apply_phase2.py # dry-run python n8n/_apply_phase2.py --apply [--activate] """ import argparse import copy import json import os import sys import uuid ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, ROOT) sys.path.insert(0, os.path.join(ROOT, "scripts")) from n8n_workflow_lib import load_credentials, N8NClient # noqa: E402 WID = "4UMRwxJdHFfOGHBp" CF_ID_BRAND = "E6lI9ykWhqpj7Pmi7Qd3" CODE_EXTRACT_NAME = "Extraer id_contacto_sucursal de Marca" IF_CF_PRESENT_NAME = "¿cfValue presente?" HTTP_GET_NAME = "Match directo por id_contacto_sucursal" IF_GET_OK_NAME = "¿GET de contacto sucursal OK?" def code_extract_node(): return { "parameters": { "jsCode": ( "var c = $('Obtener Contacto Cuenta Origen - SUCURSAL').first().json.contact || {};\n" "var cfs = c.customFields || [];\n" "var v = null;\n" "for (var i = 0; i < cfs.length; i++) {\n" " if (cfs[i].id === '" + CF_ID_BRAND + "') { v = cfs[i].value; break; }\n" "}\n" "return [{ json: { cfValue: v || null } }];" ) }, "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [1376, 416], "id": str(uuid.uuid4()), "name": CODE_EXTRACT_NAME, "notes": "FASE 2. Extrae el valor del CF id_contacto_sucursal del contacto Marca origen.", } def if_cf_present_node(): return { "parameters": { "conditions": { "options": { "caseSensitive": True, "leftValue": "", "typeValidation": "loose", "version": 3, }, "conditions": [ { "id": str(uuid.uuid4()), "leftValue": "={{ $json.cfValue }}", "rightValue": "", "operator": {"type": "string", "operation": "notEmpty", "singleValue": True}, } ], "combinator": "and", }, "options": {}, }, "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1568, 416], "id": str(uuid.uuid4()), "name": IF_CF_PRESENT_NAME, "notes": "true→HTTP GET, false→cascada email", } def http_get_node(): return { "parameters": { "method": "GET", "url": "=https://services.leadconnectorhq.com/contacts/{{ $('" + CODE_EXTRACT_NAME + "').item.json.cfValue }}", "sendHeaders": True, "headerParameters": { "parameters": [ {"name": "Version", "value": "2021-07-28"}, {"name": "Authorization", "value": "=Bearer {{ $('Datos API Cuenta objetivo - SUCURSAL').item.json['Token/API'] }}"}, ] }, "options": {"redirect": {"redirect": {}}}, }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1760, 416], "id": str(uuid.uuid4()), "name": HTTP_GET_NAME, "onError": "continueRegularOutput", "alwaysOutputData": True, "notes": "GET directo a sucursal por id (el valor del CF). Si 404, IF siguiente cae a cascada.", } def if_get_ok_node(): return { "parameters": { "conditions": { "options": { "caseSensitive": True, "leftValue": "", "typeValidation": "loose", "version": 3, }, "conditions": [ { "id": str(uuid.uuid4()), "leftValue": "={{ $json.contact && $json.contact.id ? 'ok' : '' }}", "rightValue": "", "operator": {"type": "string", "operation": "notEmpty", "singleValue": True}, } ], "combinator": "and", }, "options": {}, }, "type": "n8n-nodes-base.if", "typeVersion": 2, "position": [1952, 416], "id": str(uuid.uuid4()), "name": IF_GET_OK_NAME, "notes": "true→UPDATE (Conseguir Custom Cuenta objetivo - SUCURSAL), false→cascada email", } def main(): parser = argparse.ArgumentParser() parser.add_argument("--apply", action="store_true") parser.add_argument("--activate", action="store_true") args = parser.parse_args() if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") client = N8NClient(*load_credentials()) wf, backup = client.backup_workflow(WID, label="fase2_pre") print(f"backup: {backup}") prev = wf.get("versionId") print(f"versionId pre: {prev} active: {wf.get('active')} nodes: {len(wf['nodes'])}") wfm = copy.deepcopy(wf) for name in (CODE_EXTRACT_NAME, IF_CF_PRESENT_NAME, HTTP_GET_NAME, IF_GET_OK_NAME): try: client.assert_idempotent(wfm, name) except Exception as e: raise SystemExit(f"Aborto: {e}") # Insertar nodos code_node = code_extract_node() if_cf = if_cf_present_node() http_get = http_get_node() if_ok = if_get_ok_node() for n in (code_node, if_cf, http_get, if_ok): client.add_node(wfm, n) # Conexiones: # Datos API Cuenta objetivo - SUCURSAL → Code Extract # Code Extract → IF cfValue presente # true → HTTP GET # → IF GET OK # true → Conseguir Custom Cuenta objetivo - SUCURSAL # false → Buscar Contacto Objetivo - SUCURSAL(mail) # false → Buscar Contacto Objetivo - SUCURSAL(mail) client.set_connection(wfm, "Datos API Cuenta objetivo - SUCURSAL", CODE_EXTRACT_NAME) client.set_connection(wfm, CODE_EXTRACT_NAME, IF_CF_PRESENT_NAME) client.branch_if(wfm, IF_CF_PRESENT_NAME, true_target=HTTP_GET_NAME, false_target="Buscar Contacto Objetivo - SUCURSAL(mail)") client.set_connection(wfm, HTTP_GET_NAME, IF_GET_OK_NAME) client.branch_if(wfm, IF_GET_OK_NAME, true_target="Conseguir Custom Cuenta objetivo - SUCURSAL", false_target="Buscar Contacto Objetivo - SUCURSAL(mail)") print(f"nodes post: {len(wfm['nodes'])} (+{len(wfm['nodes'])-len(wf['nodes'])})") if not args.apply: res = client.put_workflow(WID, wfm, dry_run=True) print(f"DRY-RUN. {res['path']}") return client.put_workflow(WID, wfm, dry_run=False) wf2 = client.verify_post(WID, expected_node_names=[CODE_EXTRACT_NAME, IF_CF_PRESENT_NAME, HTTP_GET_NAME, IF_GET_OK_NAME], prev_version_id=prev) print(f"versionId nuevo: {wf2.get('versionId')} active: {wf2.get('active')}") if args.activate or not wf2.get("active"): client.activate(WID) print("activado") print("Fase 2 aplicada.") if __name__ == "__main__": main()