descripción
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Cierra el Patrón B (Fuente=REDES SOCIALES) en el workflow n8n [2004]
|
||||
(ddUEORBEtZLzsQF2).
|
||||
|
||||
Contexto: la rama "Creado por usuario" pone Canal=SUCURSAL a los WEB_USER/
|
||||
MOBILE_USER. Para un contacto con Fuente=REDES SOCIALES eso es incoherente
|
||||
(redes sociales = canal digital). Decisión de negocio (2026-05-30): tratarlos
|
||||
como digital -> Canal=FACEBOOK + Fuente=LEAD DIGITAL (igual que hizo el fix de
|
||||
datos `scripts/fix_origen_fuente_incoherencia.py` Patrón B).
|
||||
|
||||
Cambio (solo AÑADE / re-cablea, preserva el camino SUCURSAL existente):
|
||||
1. Extiende el Code node para exponer `fuenteEsRedesSociales`.
|
||||
2. Intercala, en la rama true del IF "Creado por usuario", un IF
|
||||
"Fuente es REDES SOCIALES":
|
||||
[true] -> PUT Canal=FACEBOOK -> PUT Fuente=LEAD DIGITAL (camino digital)
|
||||
[false] -> PUT Canal de Origen = SUCURSAL (camino existente intacto)
|
||||
|
||||
Antes: Creado por usuario --true--> [PUT Canal=SUCURSAL -> tags -> IF LEAD DIGITAL -> PUT Fuente=SUCURSAL]
|
||||
Ahora: Creado por usuario --true--> IF REDES SOCIALES
|
||||
--true--> PUT Canal=FACEBOOK -> PUT Fuente=LEAD DIGITAL
|
||||
--false-> [PUT Canal=SUCURSAL -> ... (igual que antes)]
|
||||
|
||||
Solo toca custom fields del contacto (no tags ni opps), igual que el fix de datos.
|
||||
[2004] dispara al CREAR contacto.
|
||||
|
||||
Uso:
|
||||
python n8n/_add_redes_sociales_branch.py # dry-run (dumpea payload)
|
||||
python n8n/_add_redes_sociales_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 N8NClient, load_credentials # noqa: E402
|
||||
|
||||
WID = "ddUEORBEtZLzsQF2"
|
||||
|
||||
# Nodos existentes.
|
||||
CODE_NODE = 'Buscar "contact.sucursal" y "contact.tienda"'
|
||||
VERIFICADOR_NODE = "Buscar Sucursal en Verificador de Sucursales"
|
||||
IF_USUARIO_NODE = "Creado por usuario (Canal de Origen)"
|
||||
PUT_CANAL_SUCURSAL_NODE = "PUT Canal de Origen = SUCURSAL"
|
||||
|
||||
# Nodos nuevos.
|
||||
IF_REDES_NODE = "Fuente es REDES SOCIALES"
|
||||
PUT_CANAL_FB_NODE = "PUT Canal de Origen = FACEBOOK"
|
||||
PUT_FUENTE_LD_NODE = "PUT Fuente de Prospecto = LEAD DIGITAL"
|
||||
|
||||
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: versión con fuente (de _add_fuente_reconcile.py) + flag fuenteEsRedesSociales.
|
||||
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"]);
|
||||
const fuenteField = findField("contact.fuente_de_prospecto", ["Fuente de Prospecto", "FUENTE DE PROSPECTO"]);
|
||||
|
||||
// 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";
|
||||
|
||||
// Valor ACTUAL de "Fuente de Prospecto" en el contacto.
|
||||
const contactCFs =
|
||||
(contactResp && contactResp.contact && contactResp.contact.customFields) ||
|
||||
(contactResp && contactResp.customFields) ||
|
||||
[];
|
||||
let fuenteActual = null;
|
||||
if (fuenteField && fuenteField.id) {
|
||||
const hit = contactCFs.find(cf => cf.id === fuenteField.id);
|
||||
fuenteActual = hit ? (hit.value != null ? hit.value : (hit.fieldValue != null ? hit.fieldValue : null)) : null;
|
||||
}
|
||||
const fuenteNorm = String(fuenteActual || "").trim().toUpperCase();
|
||||
const fuenteEsLeadDigital = fuenteNorm === "LEAD DIGITAL";
|
||||
const fuenteEsRedesSociales = fuenteNorm === "REDES SOCIALES";
|
||||
|
||||
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 ?? [],
|
||||
},
|
||||
fuente: {
|
||||
id: fuenteField?.id ?? null,
|
||||
name: fuenteField?.name ?? null,
|
||||
fieldKey: fuenteField?.fieldKey ?? null,
|
||||
picklistOptions: fuenteField?.picklistOptions ?? [],
|
||||
},
|
||||
createdBySource: createdBySource,
|
||||
esUsuario: esUsuario,
|
||||
fuenteActual: fuenteActual,
|
||||
fuenteEsLeadDigital: fuenteEsLeadDigital,
|
||||
fuenteEsRedesSociales: fuenteEsRedesSociales,
|
||||
},
|
||||
},
|
||||
];"""
|
||||
|
||||
|
||||
def put_cf_node(name, position, *, code_field, value):
|
||||
"""Nodo httpRequest PUT /contacts/{id} que setea un custom field por id+key."""
|
||||
body = (
|
||||
"={\n"
|
||||
' "customFields": [\n'
|
||||
" {\n"
|
||||
' "id": "{{ ' + CODE_REF + "." + code_field + '.id }}",\n'
|
||||
' "key": "{{ ' + CODE_REF + "." + code_field + '.fieldKey }}",\n'
|
||||
' "field_value": "' + value + '"\n'
|
||||
" }\n"
|
||||
" ]\n"
|
||||
"}"
|
||||
)
|
||||
return {
|
||||
"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": body,
|
||||
"options": {"redirect": {"redirect": {}}},
|
||||
},
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": position,
|
||||
"name": name,
|
||||
}
|
||||
|
||||
|
||||
def build_nodes():
|
||||
if_redes = {
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": True, "leftValue": "", "typeValidation": "loose", "version": 3},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "fuente-es-redes-sociales",
|
||||
"leftValue": "={{ " + CODE_REF + ".fuenteEsRedesSociales }}",
|
||||
"rightValue": "",
|
||||
"operator": {"type": "boolean", "operation": "true", "singleValue": True},
|
||||
}
|
||||
],
|
||||
"combinator": "and",
|
||||
},
|
||||
"options": {},
|
||||
},
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.3,
|
||||
"position": [820, -320],
|
||||
"name": IF_REDES_NODE,
|
||||
}
|
||||
put_canal_fb = put_cf_node(PUT_CANAL_FB_NODE, [1040, -320], code_field="canal", value="FACEBOOK")
|
||||
put_fuente_ld = put_cf_node(PUT_FUENTE_LD_NODE, [1260, -320], code_field="fuente", value="LEAD DIGITAL")
|
||||
return if_redes, put_canal_fb, put_fuente_ld
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Cierra Patrón B (REDES SOCIALES) en [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="redes_sociales")
|
||||
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.
|
||||
for nm in (IF_REDES_NODE, PUT_CANAL_FB_NODE, PUT_FUENTE_LD_NODE):
|
||||
if client.find_node(wf, nm) is not None:
|
||||
raise SystemExit(f"El nodo {nm!r} ya existe; ya fue aplicado. Nada que hacer.")
|
||||
|
||||
# Pre-requisitos.
|
||||
for nm in (IF_USUARIO_NODE, PUT_CANAL_SUCURSAL_NODE):
|
||||
if client.find_node(wf, nm) is None:
|
||||
raise SystemExit(f"No existe {nm!r}; corre antes _add_canal_origen_branch.py.")
|
||||
|
||||
# Verifica que el IF "Creado por usuario" hoy conecta su rama true a PUT Canal=SUCURSAL.
|
||||
cur = client.get_connection_targets(wf, IF_USUARIO_NODE, output_index=0)
|
||||
cur_names = [t.get("node") for t in cur]
|
||||
if cur_names != [PUT_CANAL_SUCURSAL_NODE]:
|
||||
raise SystemExit(f"Topología inesperada: {IF_USUARIO_NODE!r} true -> {cur_names} (esperaba [{PUT_CANAL_SUCURSAL_NODE!r}]).")
|
||||
|
||||
# 1. Extender Code node.
|
||||
code_node = client.find_node(wf, CODE_NODE)
|
||||
code_node["parameters"]["jsCode"] = NEW_JSCODE
|
||||
print(f" Code node {CODE_NODE!r}: jsCode extendido (fuenteEsRedesSociales).")
|
||||
|
||||
# 2. Añadir nodos.
|
||||
if_redes, put_canal_fb, put_fuente_ld = build_nodes()
|
||||
for n in (if_redes, put_canal_fb, put_fuente_ld):
|
||||
client.assert_idempotent(wf, n["name"])
|
||||
client.add_node(wf, n)
|
||||
print(" Nodos añadidos: IF REDES + PUT Canal=FACEBOOK + PUT Fuente=LEAD DIGITAL.")
|
||||
|
||||
# 3. Re-cablear: Creado por usuario[true] -> IF REDES;
|
||||
# IF REDES[true] -> PUT Canal=FACEBOOK -> PUT Fuente=LEAD DIGITAL;
|
||||
# IF REDES[false] -> PUT Canal=SUCURSAL (camino existente).
|
||||
client.set_connection(wf, IF_USUARIO_NODE, IF_REDES_NODE, output_index=0)
|
||||
client.set_connection(wf, IF_REDES_NODE, PUT_CANAL_FB_NODE, output_index=0)
|
||||
client.set_connection(wf, IF_REDES_NODE, PUT_CANAL_SUCURSAL_NODE, output_index=1)
|
||||
client.set_connection(wf, PUT_CANAL_FB_NODE, PUT_FUENTE_LD_NODE)
|
||||
print(" Conexiones re-cableadas.")
|
||||
|
||||
expected = [IF_REDES_NODE, PUT_CANAL_FB_NODE, PUT_FUENTE_LD_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: Patrón B cerrado y verificado. Backup en:", backup_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user