# -*- coding: utf-8 -*- """Fallback de autoenlace para el workflow Sucursal->Marca (x4DqZ5FtSc43tdzB). Inserta entre 'Mapear Custom Fields Cuenta Origen - SUCURSAL' y 'DATOS CUENTA OBJETIVO' un IF + PUT condicional: - Si el CF 'contact.id_contacto_sucursal' ya == contact.id -> bypass directo. - Si NO -> PUT autoenlace en la sucursal antes de continuar. Defensivo contra automatizaciones nativas de GHL que crean contactos sin disparar nuestro fill. onError=continueRegularOutput para no romper el sync si el PUT falla. Modo dry-run por defecto (dumpea n8n/dryrun_*.json sin tocar la API). Pasar --apply para PUT real + reactivar. """ import argparse import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from scripts.n8n_workflow_lib import load_credentials, N8NClient import uuid WID = "x4DqZ5FtSc43tdzB" NAME_IF = "ya esta el CF id_contacto_sucursal" NAME_PUT = "Autoenlace CF id_contacto_sucursal - SUCURSAL" SRC = "Mapear Custom Fields Cuenta Origen - SUCURSAL" DST = "DATOS CUENTA OBJETIVO" def build_if_node(): return { "parameters": { "conditions": { "options": { "caseSensitive": True, "leftValue": "", "typeValidation": "strict", "version": 2, }, "conditions": [ { "id": str(uuid.uuid4()), "leftValue": "={{ $json['contact.id_contacto_sucursal'] }}", "rightValue": "={{ $json.id }}", "operator": { "type": "string", "operation": "equals", "name": "filter.operator.equals", }, } ], "combinator": "and", }, "options": {}, }, "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [1760, -288], "id": str(uuid.uuid4()), "name": NAME_IF, "notes": ( "Fallback defensivo. Si el CF 'contact.id_contacto_sucursal' ya == contact.id " "(estado normal poblado por fill_contact_id_sucursal.py), pasa directo. " "Si esta vacio o distinto (creacion por automatizacion nativa de GHL que se " "salto nuestro fill), va a la rama PUT para autoenlazar antes de tocar Marca." ), } def build_put_node(): json_body = ( "={{ JSON.stringify({ customFields: [ { " "id: $('Conseguir Custom Cuenta Origen- SUCURSAL').first().json.customFields" ".find(f => f.fieldKey === 'contact.id_contacto_sucursal')?.id, " "key: 'contact.id_contacto_sucursal', " "field_value: $('Mapear Custom Fields Cuenta Origen - SUCURSAL').item.json.id" " } ] }) }}" ) return { "parameters": { "method": "PUT", "url": ( "=https://services.leadconnectorhq.com/contacts/" "{{ $('Mapear Custom Fields Cuenta Origen - SUCURSAL').item.json.id }}" ), "sendHeaders": True, "headerParameters": { "parameters": [ {"name": "Accept", "value": "application/json"}, {"name": "Version", "value": "2021-07-28"}, { "name": "Authorization", "value": ( "=Bearer " "{{ $('Obtener Info de cuenta origen - SUCURSAL').item.json.API_token }}" ), }, {"name": "Content-Type", "value": "application/json"}, ] }, "sendBody": True, "specifyBody": "json", "jsonBody": json_body, "options": {}, }, "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [1936, -192], "id": str(uuid.uuid4()), "name": NAME_PUT, "onError": "continueRegularOutput", "notes": ( "Autoenlace de seguridad: setea CF 'contact.id_contacto_sucursal' = " "contact.id propio en la sucursal. Solo se ejecuta si el IF previo " "detecto que el CF estaba vacio o distinto. onError=continue para no " "romper el sync si el PUT falla." ), } def parse_args(): ap = argparse.ArgumentParser() ap.add_argument("--apply", action="store_true", help="Aplicar PUT real (default: dry-run a n8n/dryrun_*.json)") return ap.parse_args() def main(): args = parse_args() api_key, base_url = load_credentials() client = N8NClient(api_key, base_url) print(f"[1/6] GET workflow {WID}") wf = client.get_workflow(WID) print(f" versionId previo: {wf.get('versionId')}") print(f" active : {wf.get('active')}") print(f" total nodos : {len(wf.get('nodes') or [])}") existing = {n["name"] for n in wf.get("nodes") or []} if NAME_IF in existing or NAME_PUT in existing: print(f"\nERROR: los nodos ya existen ({NAME_IF!r} o {NAME_PUT!r}). Abortando.") sys.exit(2) if SRC not in existing: print(f"\nERROR: no encontre el nodo source {SRC!r}. Abortando.") sys.exit(2) if DST not in existing: print(f"\nERROR: no encontre el nodo destino {DST!r}. Abortando.") sys.exit(2) print(f"\n[2/6] Backup fresco") _, bpath = client.backup_workflow(WID, label="pre_fallback_autoenlace") print(f" backup -> {bpath}") print(f"\n[3/6] Inyectar nodos IF + PUT") if_node = build_if_node() put_node = build_put_node() client.add_node(wf, if_node) client.add_node(wf, put_node) print(f" IF : {NAME_IF}") print(f" PUT: {NAME_PUT}") print(f"\n[4/6] Reconectar grafo") # Reemplaza SRC -> DST por SRC -> IF client.insert_between(wf, SRC, NAME_IF, DST) # IF.output(0)=true (CF ya ok) -> DST; IF.output(1)=false (CF vacio/distinto) -> PUT -> DST client.branch_if(wf, NAME_IF, true_target=DST, false_target=NAME_PUT) # PUT -> DST (convergencia) conns = wf["connections"] conns[NAME_PUT] = {"main": [[{"node": DST, "type": "main", "index": 0}]]} print(f" {SRC} -> {NAME_IF}") print(f" {NAME_IF} [true] -> {DST}") print(f" {NAME_IF} [false] -> {NAME_PUT}") print(f" {NAME_PUT} -> {DST}") print(f"\n[5/6] {'APPLY' if args.apply else 'DRY-RUN'} put_workflow") result = client.put_workflow(WID, wf, dry_run=not args.apply) if not args.apply: print(f" dry-run dump -> {result}") print("\nLISTO. Revisa el JSON. Si todo bien, corre con --apply.") return print(f"\n[6/6] Verificar y reactivar") new_wf = client.verify_post( WID, expected_node_names=[NAME_IF, NAME_PUT], prev_version_id=wf.get("versionId"), ) print(f" versionId nuevo: {new_wf.get('versionId')}") if not new_wf.get("active"): print(" workflow quedo inactivo tras PUT; reactivando...") client.activate(WID) new_wf = client.get_workflow(WID) print(f" active final : {new_wf.get('active')}") print(f" total nodos : {len(new_wf.get('nodes') or [])}") print("\nOK.") if __name__ == "__main__": main()