#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Complementa el workflow n8n [2004] (ddUEORBEtZLzsQF2) para que, al crear un contacto en sucursal, además de rellenar contact.sucursal / contact.tienda, escriba "Canal de Origen" = SUCURSAL y deje el tag de origen correcto, PERO solo cuando el contacto fue creado por un usuario (createdBy.source ∈ {WEB_USER, MOBILE_USER}). Los creados por integración (INTEGRATION) no se tocan. Replica en TIEMPO REAL el criterio del batch scripts/fix_branch_user_origin.py. Cambios (solo AÑADE, preserva el flujo actual): 1. Extiende el Code node "Buscar contact.sucursal y contact.tienda" para resolver también el field "Canal de Origen" (por fieldKey con fallback a nombre) y exponer createdBySource / esUsuario (leído del GET del contacto). 2. Tras el PUT actual de sucursal/tienda añade: IF "Creado por usuario" -> [true] PUT Canal de Origen = SUCURSAL -> Tag+ sucursal -> Tag- formulario -> Tag- facebook-ads [false] (fin: no se toca). Uso: python n8n/_add_canal_origen_branch.py # dry-run (dumpea payload) python n8n/_add_canal_origen_branch.py --apply # aplica + reactiva """ import argparse import os import sys ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if ROOT_DIR not in sys.path: sys.path.insert(0, ROOT_DIR) from scripts.n8n_workflow_lib import ( # noqa: E402 AlreadyExistsError, N8NClient, load_credentials, ) WID = "ddUEORBEtZLzsQF2" # Nodos existentes (referencias). CODE_NODE = 'Buscar "contact.sucursal" y "contact.tienda"' GET_CONTACT_NODE = "Obtener Contacto Cuenta Origen - SUCURSAL" PUT_SUCURSAL_NODE = "Actualizar Contacto Cuenta Objetivo - SUCURSAL" VERIFICADOR_NODE = "Buscar Sucursal en Verificador de Sucursales" # Nodos nuevos. IF_NODE = "Creado por usuario (Canal de Origen)" PUT_CANAL_NODE = "PUT Canal de Origen = SUCURSAL" TAG_ADD_NODE = "Tag+ sucursal" TAG_RM_FORM_NODE = "Tag- formulario" TAG_RM_FB_NODE = "Tag- facebook-ads" # Expresiones reutilizables (referencian nodos upstream por nombre). CONTACT_ID_EXPR = "{{ $('Datos de Lead').item.json.Cliente['Contact ID'] }}" TOKEN_EXPR = "{{ $('" + VERIFICADOR_NODE + "').item.json['SC TOKEN BUCEFALO'] }}" CODE_REF = "$('" + CODE_NODE + "').item.json" HEADERS = [ {"name": "Accept", "value": "application/json"}, {"name": "Version", "value": "2021-07-28"}, {"name": "Authorization", "value": "=Bearer " + TOKEN_EXPR}, ] # Code node extendido: preserva sucursal/tienda EXACTO y añade canal + esUsuario. NEW_JSCODE = r"""const customFields = $input.first().json.customFields; function findField(key, names) { let f = customFields.find(x => x.fieldKey === key); if (!f) { const wanted = names.map(n => n.toLowerCase().trim()); f = customFields.find(x => wanted.includes((x.name || "").toLowerCase().trim())); } return f || null; } const sucursalField = findField("contact.sucursal", ["Sucursal"]); const tiendaField = findField("contact.tienda", ["TIENDA", "Tienda"]); const canalField = findField("contact.fuente_de_posible_cliente", ["CANAL DE ORIGEN", "Canal de Origen"]); // createdBy.source SOLO viene del GET individual del contacto. const contactResp = $('Obtener Contacto Cuenta Origen - SUCURSAL').item.json; const createdBySource = (contactResp && contactResp.contact && contactResp.contact.createdBy && contactResp.contact.createdBy.source) || (contactResp && contactResp.createdBy && contactResp.createdBy.source) || null; const esUsuario = createdBySource === "WEB_USER" || createdBySource === "MOBILE_USER"; return [ { json: { sucursal: { id: sucursalField?.id ?? null, name: sucursalField?.name ?? null, fieldKey: sucursalField?.fieldKey ?? null, picklistOptions: sucursalField?.picklistOptions ?? [], }, tienda: { id: tiendaField?.id ?? null, name: tiendaField?.name ?? null, fieldKey: tiendaField?.fieldKey ?? null, picklistOptions: tiendaField?.picklistOptions ?? [], }, canal: { id: canalField?.id ?? null, name: canalField?.name ?? null, fieldKey: canalField?.fieldKey ?? null, picklistOptions: canalField?.picklistOptions ?? [], }, createdBySource: createdBySource, esUsuario: esUsuario, }, }, ];""" def http_node(name, position, *, method, body_obj_expr, on_error=False): """Construye un nodo httpRequest (typeVersion 4.2) con auth Bearer en header.""" params = { "method": method, "url": "=https://services.leadconnectorhq.com/contacts/" + CONTACT_ID_EXPR + ("/tags" if method in ("POST", "DELETE") else ""), "sendHeaders": True, "headerParameters": {"parameters": [dict(h) for h in HEADERS]}, "sendBody": True, "specifyBody": "json", "jsonBody": body_obj_expr, "options": {"redirect": {"redirect": {}}}, } node = { "parameters": params, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": position, "name": name, } if on_error: # Un DELETE de un tag inexistente puede no aplicar; no debe romper el flujo. node["onError"] = "continueRegularOutput" return node def build_nodes(): # PUT Canal de Origen = SUCURSAL (solo este CF; NO toca Fuente de Prospecto). canal_body = ( "={\n" ' "customFields": [\n' " {\n" ' "id": "{{ ' + CODE_REF + '.canal.id }}",\n' ' "key": "{{ ' + CODE_REF + '.canal.fieldKey }}",\n' ' "field_value": "SUCURSAL"\n' " }\n" " ]\n" "}" ) put_canal = { "parameters": { "method": "PUT", "url": "=https://services.leadconnectorhq.com/contacts/" + CONTACT_ID_EXPR, "sendHeaders": True, "headerParameters": {"parameters": [dict(h) for h in HEADERS]}, "sendBody": True, "specifyBody": "json", "jsonBody": canal_body, "options": {"redirect": {"redirect": {}}}, }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [940, -120], "name": PUT_CANAL_NODE, } if_node = { "parameters": { "conditions": { "options": {"caseSensitive": True, "leftValue": "", "typeValidation": "loose", "version": 3}, "conditions": [ { "id": "canal-origen-esusuario", "leftValue": "={{ " + CODE_REF + ".esUsuario }}", "rightValue": "", "operator": {"type": "boolean", "operation": "true", "singleValue": True}, } ], "combinator": "and", }, "options": {}, }, "type": "n8n-nodes-base.if", "typeVersion": 2.3, "position": [720, -120], "name": IF_NODE, } tag_add = http_node(TAG_ADD_NODE, [1160, -120], method="POST", body_obj_expr='={\n "tags": ["sucursal"]\n}') tag_rm_form = http_node(TAG_RM_FORM_NODE, [1380, -120], method="DELETE", body_obj_expr='={\n "tags": ["formulario"]\n}', on_error=True) tag_rm_fb = http_node(TAG_RM_FB_NODE, [1600, -120], method="DELETE", body_obj_expr='={\n "tags": ["facebook-ads"]\n}', on_error=True) return if_node, put_canal, tag_add, tag_rm_form, tag_rm_fb def main(): parser = argparse.ArgumentParser(description="Añade la rama Canal de Origen al workflow n8n [2004].") parser.add_argument("--apply", action="store_true", help="Aplica el PUT real (sin esto: dry-run).") args = parser.parse_args() client = N8NClient(*load_credentials()) wf, backup_path = client.backup_workflow(WID, label="canal_origen") prev_version = wf.get("versionId") print(f"Workflow: {wf.get('name')}") print(f" active={wf.get('active')} nodes={len(wf.get('nodes') or [])} versionId={prev_version}") print(f" backup -> {backup_path}") # Idempotencia: si ya están los nodos nuevos, no re-aplicar. for nm in (IF_NODE, PUT_CANAL_NODE, TAG_ADD_NODE): if client.find_node(wf, nm) is not None: raise SystemExit(f"El nodo {nm!r} ya existe; el complemento ya fue aplicado. Nada que hacer.") # 1. Extender el Code node. code_node = client.find_node(wf, CODE_NODE) if code_node is None: raise SystemExit(f"No se encontró el Code node {CODE_NODE!r}.") code_node["parameters"]["jsCode"] = NEW_JSCODE print(f" Code node {CODE_NODE!r}: jsCode extendido (canal + esUsuario).") # 2. Añadir nodos nuevos. if_node, put_canal, tag_add, tag_rm_form, tag_rm_fb = build_nodes() for n in (if_node, put_canal, tag_add, tag_rm_form, tag_rm_fb): client.assert_idempotent(wf, n["name"]) client.add_node(wf, n) print(" Nodos añadidos: IF + PUT canal + 3 de tags.") # 3. Conexiones: PUT sucursal -> IF -> [true] PUT canal -> tag+ -> tag- form -> tag- fb. client.set_connection(wf, PUT_SUCURSAL_NODE, IF_NODE) # main:0 client.set_connection(wf, IF_NODE, PUT_CANAL_NODE, output_index=0) # rama true # rama false (index 1): sin destino (fin). client.set_connection(wf, PUT_CANAL_NODE, TAG_ADD_NODE) client.set_connection(wf, TAG_ADD_NODE, TAG_RM_FORM_NODE) client.set_connection(wf, TAG_RM_FORM_NODE, TAG_RM_FB_NODE) print(" Conexiones cableadas.") expected = [IF_NODE, PUT_CANAL_NODE, TAG_ADD_NODE, TAG_RM_FORM_NODE, TAG_RM_FB_NODE] if not args.apply: res = client.put_workflow(WID, wf, dry_run=True) print(f"\nDRY-RUN. Payload -> {res['path']} ({res['node_count']} nodos).") print("Revisa el JSON y vuelve a correr con --apply para aplicar.") return was_active = bool(wf.get("active")) if was_active: try: client.deactivate(WID) print(" Workflow desactivado para PUT estructural.") except Exception as exc: print(f" WARN al desactivar: {exc}") client.put_workflow(WID, wf, dry_run=False) print(" PUT aplicado.") if was_active: client.activate(WID) print(" Workflow reactivado.") client.verify_post(WID, expected_node_names=expected, prev_version_id=prev_version) print("\nOK: complemento aplicado y verificado. Backup en:", backup_path) if __name__ == "__main__": main()