Primer commit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Fase 1 — SUCURSAL → MARCA: match primario por id_contacto_sucursal.
|
||||
|
||||
Insertar entre `Datos API Cuenta objetivo - MARCA` y `Buscar Contacto Objetivo
|
||||
- MARCA (phone)`:
|
||||
1) HTTP search por CF
|
||||
2) IF que decide CF match único
|
||||
- true → Capturar ID Match (reutilizado)
|
||||
- false → Buscar Contacto Objetivo - MARCA (phone) (cascada existente)
|
||||
|
||||
Modificar los code nodes de CREATE y UPDATE para que inyecten el CF
|
||||
`contact.id_contacto_sucursal` con el id del contacto origen, garantizando
|
||||
enlace incluso si el match cayó a la cascada.
|
||||
|
||||
Uso:
|
||||
python n8n/_apply_phase1.py # dry-run
|
||||
python n8n/_apply_phase1.py --apply
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
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 = "x4DqZ5FtSc43tdzB"
|
||||
CF_ID_BRAND = "E6lI9ykWhqpj7Pmi7Qd3"
|
||||
|
||||
NEW_HTTP_NAME = "Buscar Contacto Objetivo - MARCA (id_contacto_sucursal)"
|
||||
NEW_IF_NAME = "¿id_contacto_sucursal match único?"
|
||||
|
||||
|
||||
def http_node_search_by_cf():
|
||||
return {
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://services.leadconnectorhq.com/contacts/search",
|
||||
"sendHeaders": True,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Version", "value": "2021-07-28"},
|
||||
{"name": "Authorization",
|
||||
"value": "=Bearer {{ $('Datos API Cuenta objetivo - MARCA').item.json['Token/API'] }}"},
|
||||
]
|
||||
},
|
||||
"sendBody": True,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": (
|
||||
"={\n"
|
||||
" \"locationId\": \"{{ $('Datos API Cuenta objetivo - MARCA').item.json['Location ID'] }}\",\n"
|
||||
" \"pageLimit\": 5,\n"
|
||||
" \"filters\": [\n"
|
||||
" {\n"
|
||||
" \"group\": \"AND\",\n"
|
||||
" \"filters\": [\n"
|
||||
" {\n"
|
||||
" \"field\": \"customFields." + CF_ID_BRAND + "\",\n"
|
||||
" \"operator\": \"eq\",\n"
|
||||
" \"value\": \"{{ $('Crear Contacto').item.json.body.contact_id }}\"\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}"
|
||||
),
|
||||
"options": {"redirect": {"redirect": {}}},
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2832, -496],
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": NEW_HTTP_NAME,
|
||||
"onError": "continueRegularOutput",
|
||||
"alwaysOutputData": True,
|
||||
"notes": (
|
||||
"FASE 1 — Match primario por contact.id_contacto_sucursal. "
|
||||
"Si total==1, el siguiente IF deriva a Capturar ID Match. "
|
||||
"Si no, sigue a la cascada phone→email→nombre."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def if_node_cf_match():
|
||||
return {
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": True,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 3,
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"leftValue": "={{ $json.total }}",
|
||||
"rightValue": 1,
|
||||
"operator": {"type": "number", "operation": "equals"},
|
||||
}
|
||||
],
|
||||
"combinator": "and",
|
||||
},
|
||||
"options": {},
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [3040, -496],
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": NEW_IF_NAME,
|
||||
"notes": "FASE 1. true → Capturar ID Match. false → cascada phone.",
|
||||
}
|
||||
|
||||
|
||||
# ---- Patch helpers para code nodes ----
|
||||
|
||||
INJECT_BLOCK_UPDATE = (
|
||||
"\n// === FASE 1: inyectar CF id_contacto_sucursal (defensive) ===\n"
|
||||
"var __linkId = '" + CF_ID_BRAND + "';\n"
|
||||
"customFields = customFields.filter(function(c){ return c.id !== __linkId; });\n"
|
||||
"if (origenData && origenData.id) {\n"
|
||||
" customFields.push({ id: __linkId, key: 'contact.id_contacto_sucursal', field_value: origenData.id });\n"
|
||||
"}\n"
|
||||
)
|
||||
|
||||
INJECT_BLOCK_CREATE_FK = (
|
||||
"\n// === FASE 1: garantizar CF contact.id_contacto_sucursal ===\n"
|
||||
"(function(){\n"
|
||||
" var origenId = ($('Obtener Contacto Cuenta Origen - SUCURSAL').first().json.contact || {}).id;\n"
|
||||
" if (!origenId) return;\n"
|
||||
" // Encontrar el def en Marca por fieldKey.\n"
|
||||
" var defLink = marcaCustomDefs.find(function(d){ return d.fieldKey === 'contact.id_contacto_sucursal'; });\n"
|
||||
" if (!defLink) return;\n"
|
||||
" customFields = customFields.filter(function(c){ return c.id !== defLink.id; });\n"
|
||||
" customFields.push({ id: defLink.id, field_value: origenId });\n"
|
||||
"})();\n"
|
||||
)
|
||||
|
||||
|
||||
def patch_update_code(wf, client):
|
||||
"""Modifica `Obtener Body para Actualizar Contacto Objetivo - MARCA`."""
|
||||
node = client.find_node(wf, "Obtener Body para Actualizar Contacto Objetivo - MARCA")
|
||||
if not node:
|
||||
raise RuntimeError("nodo update no encontrado")
|
||||
code = node["parameters"].get("jsCode") or ""
|
||||
if "FASE 1: inyectar CF id_contacto_sucursal" in code:
|
||||
print(" [skip] code update ya tiene el bloque FASE 1")
|
||||
return False
|
||||
# Insertar antes de `var body = {`
|
||||
marker = "var body = {"
|
||||
idx = code.rfind(marker)
|
||||
if idx < 0:
|
||||
raise RuntimeError("marker 'var body = {' no encontrado en code update")
|
||||
new_code = code[:idx] + INJECT_BLOCK_UPDATE + code[idx:]
|
||||
node["parameters"]["jsCode"] = new_code
|
||||
return True
|
||||
|
||||
|
||||
def patch_create_code(wf, client):
|
||||
"""Modifica `Obtener el body para crear Contacto - MARCA`."""
|
||||
node = client.find_node(wf, "Obtener el body para crear Contacto - MARCA")
|
||||
if not node:
|
||||
raise RuntimeError("nodo create no encontrado")
|
||||
code = node["parameters"].get("jsCode") or ""
|
||||
if "FASE 1: garantizar CF contact.id_contacto_sucursal" in code:
|
||||
print(" [skip] code create ya tiene el bloque FASE 1")
|
||||
return False
|
||||
marker = "var body = {"
|
||||
idx = code.rfind(marker)
|
||||
if idx < 0:
|
||||
raise RuntimeError("marker 'var body = {' no encontrado en code create")
|
||||
new_code = code[:idx] + INJECT_BLOCK_CREATE_FK + code[idx:]
|
||||
node["parameters"]["jsCode"] = new_code
|
||||
return True
|
||||
|
||||
|
||||
# ---- Main ----
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--apply", action="store_true", help="Aplica al workflow. Sin esto: dry-run.")
|
||||
parser.add_argument("--activate", action="store_true", help="Tras apply OK, activar el workflow.")
|
||||
args = parser.parse_args()
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
client = N8NClient(*load_credentials())
|
||||
print(f"[fase 1] GET workflow {WID}")
|
||||
wf, backup_path = client.backup_workflow(WID, label="fase1_pre")
|
||||
prev_version = wf.get("versionId")
|
||||
print(f" backup: {backup_path}")
|
||||
print(f" versionId: {prev_version} active: {wf.get('active')}")
|
||||
print(f" nodes pre: {len(wf['nodes'])}")
|
||||
|
||||
# Trabajar sobre copia
|
||||
wfm = copy.deepcopy(wf)
|
||||
|
||||
# Idempotencia
|
||||
try:
|
||||
client.assert_idempotent(wfm, NEW_HTTP_NAME)
|
||||
client.assert_idempotent(wfm, NEW_IF_NAME)
|
||||
except Exception as e:
|
||||
raise SystemExit(f"Aborto: {e}")
|
||||
|
||||
# Insertar nodos
|
||||
print("[fase 1] insertando nodos nuevos...")
|
||||
http_node = http_node_search_by_cf()
|
||||
if_node = if_node_cf_match()
|
||||
client.add_node(wfm, http_node)
|
||||
client.add_node(wfm, if_node)
|
||||
|
||||
# Reconectar:
|
||||
# antes: Datos API Cuenta objetivo - MARCA → Buscar Contacto Objetivo - MARCA (phone)
|
||||
# ahora: Datos API → HTTP nuevo → IF nuevo
|
||||
# IF true → Capturar ID Match
|
||||
# IF false → Buscar Contacto Objetivo - MARCA (phone)
|
||||
client.set_connection(wfm, "Datos API Cuenta objetivo - MARCA", NEW_HTTP_NAME)
|
||||
client.set_connection(wfm, NEW_HTTP_NAME, NEW_IF_NAME)
|
||||
client.branch_if(wfm, NEW_IF_NAME,
|
||||
true_target="Capturar ID Match",
|
||||
false_target="Buscar Contacto Objetivo - MARCA (phone)")
|
||||
|
||||
# Modificar code nodes
|
||||
print("[fase 1] modificando code UPDATE...")
|
||||
patch_update_code(wfm, client)
|
||||
print("[fase 1] modificando code CREATE...")
|
||||
patch_create_code(wfm, client)
|
||||
|
||||
print(f"[fase 1] nodes post: {len(wfm['nodes'])} (+{len(wfm['nodes'])-len(wf['nodes'])})")
|
||||
|
||||
# Dry-run
|
||||
print("[fase 1] dry-run PUT (dump a archivo)...")
|
||||
res = client.put_workflow(WID, wfm, dry_run=True)
|
||||
print(f" dry-run path: {res['path']}")
|
||||
|
||||
if not args.apply:
|
||||
print("\nDRY-RUN. Para aplicar: --apply [--activate]")
|
||||
return
|
||||
|
||||
# Apply real
|
||||
print("[fase 1] PUT real...")
|
||||
client.put_workflow(WID, wfm, dry_run=False)
|
||||
print(" PUT OK. Verificando...")
|
||||
wf_post = client.verify_post(
|
||||
WID,
|
||||
expected_node_names=[NEW_HTTP_NAME, NEW_IF_NAME],
|
||||
prev_version_id=prev_version,
|
||||
)
|
||||
print(f" versionId nuevo: {wf_post.get('versionId')}")
|
||||
print(f" active post-PUT: {wf_post.get('active')}")
|
||||
|
||||
if args.activate or not wf_post.get("active"):
|
||||
print("[fase 1] activando workflow...")
|
||||
client.activate(WID)
|
||||
wf_post2 = client.get_workflow(WID)
|
||||
print(f" active final: {wf_post2.get('active')}")
|
||||
|
||||
print("\n[fase 1] aplicado y activado.")
|
||||
print("Siguiente paso: python scripts/n8n_e2e_test.py --scenario all-phase1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user