Primer commit
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Endurece el workflow de sync de opps (Cfgwp0bOtDW8zuKW) con idempotencia
|
||||
GLOBAL via mapeo Baserow, para que NO vuelva a crear replicas duplicadas en
|
||||
Marca cuando la identidad del contacto es ambigua.
|
||||
|
||||
Problema (caso 2026-05-30): el nodo 'Decidir Match (Create vs Update)' busca la
|
||||
opp existente SOLO entre las opps del contacto resuelto ese run. Si resuelve un
|
||||
contacto distinto al de la opp original (mismo telefono/nombre variante), no la
|
||||
encuentra -> CREATE -> duplicado (descuadre positivo Marca>Sucursales).
|
||||
|
||||
Fix:
|
||||
1. Nuevo nodo Baserow 'Buscar Mapeo Opp - Baserow' (getAll, tabla 754,
|
||||
filtro id_opp_sucursal == id de la opp de sucursal). alwaysOutputData=true
|
||||
+ onError=continueRegularOutput (si falla o no hay match, sigue el flujo).
|
||||
2. Se inserta EN SERIE: Set Contact ID Resuelto -> [Buscar Mapeo Opp] ->
|
||||
Buscar Oportunidades del Contacto - MARCA -> Decidir Match. Las refs por
|
||||
nombre ($('NodoX')) de los nodos siguientes se mantienen intactas.
|
||||
3. 'Decidir Match' reescrito: si el mapeo Baserow tiene id_opp_marca -> UPDATE
|
||||
esa opp (global, independiente del contacto); si no, cae al match por CF
|
||||
entre las opps del contacto (logica previa); si nada -> CREATE.
|
||||
|
||||
La frescura del mapeo (opps nuevas) la cubre el backfill idempotente agendado
|
||||
(scripts/backfill_baserow_opp_mapping.py --table-id 754), no un nodo create
|
||||
fragil dentro del flujo de produccion.
|
||||
|
||||
Dry-run por defecto (dump a n8n/dryrun_*.json); --apply para PUT real.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from scripts.n8n_workflow_lib import load_credentials, N8NClient
|
||||
|
||||
WID = "Cfgwp0bOtDW8zuKW"
|
||||
BASEROW_DB = 63
|
||||
MAPPING_TABLE = 754
|
||||
FIELD_ID_OPP_SUCURSAL = 7280 # campo "id_opp_sucursal" (primario) en tabla 754
|
||||
BASEROW_CRED = {"baserowApi": {"id": "LZztQ3WMpzXjSTIH", "name": "Baserow account"}}
|
||||
|
||||
NAME_LOOKUP = "Buscar Mapeo Opp - Baserow"
|
||||
SRC = "Set Contact ID Resuelto"
|
||||
NEXT = "Buscar Oportunidades del Contacto - MARCA"
|
||||
DECIDE = "Decidir Match (Create vs Update)"
|
||||
|
||||
NEW_DECIDE_CODE = """// DECISION (Create vs Update) con idempotencia GLOBAL via mapeo Baserow (tabla 754).
|
||||
// (1) Si el mapeo id_opp_sucursal -> id_opp_marca existe -> UPDATE esa opp de Marca,
|
||||
// independiente del contacto resuelto (evita duplicados por contacto ambiguo).
|
||||
// (2) Si no hay mapeo, match por CF entre las opps del contacto resuelto.
|
||||
// (3) Si nada -> CREATE.
|
||||
const result = $input.first().json;
|
||||
const opportunities = result.opportunities || [];
|
||||
const sucursalOppId = $('Datos de Lead').first().json.Oportunidad.opportunity_id;
|
||||
|
||||
let action = 'CREATE';
|
||||
let opportunityId = null;
|
||||
let matchReason = '';
|
||||
|
||||
// (1) Mapeo Baserow global
|
||||
let baserowRows = [];
|
||||
try { baserowRows = $('Buscar Mapeo Opp - Baserow').all().map(i => i.json); } catch (e) { baserowRows = []; }
|
||||
const mapped = baserowRows.find(r =>
|
||||
String(r.id_opp_sucursal || '') === String(sucursalOppId) && String(r.id_opp_marca || '').trim()
|
||||
);
|
||||
|
||||
if (mapped) {
|
||||
action = 'UPDATE';
|
||||
opportunityId = String(mapped.id_opp_marca).trim();
|
||||
matchReason = 'Match por mapeo Baserow (global, tabla 754)';
|
||||
} else {
|
||||
// (2) match por CF entre las opps del contacto resuelto
|
||||
const match = opportunities.find(o =>
|
||||
(o.customFields || []).some(cf => {
|
||||
const v = cf.fieldValueString ?? cf.fieldValue ?? '';
|
||||
return String(v) === String(sucursalOppId);
|
||||
})
|
||||
);
|
||||
if (match) {
|
||||
action = 'UPDATE';
|
||||
opportunityId = match.id;
|
||||
matchReason = 'Match por ID Oportunidad Sucursal (contacto)';
|
||||
} else {
|
||||
action = 'CREATE';
|
||||
matchReason = 'Sin match (Baserow ni contacto) -> CREATE (contacto tiene ' + opportunities.length + ' opps)';
|
||||
}
|
||||
}
|
||||
|
||||
return [{
|
||||
json: {
|
||||
action,
|
||||
opportunityId,
|
||||
matchReason,
|
||||
sucursalOppId,
|
||||
contactId: $('Set Contact ID Resuelto').first().json.contactId,
|
||||
totalOppsContacto: opportunities.length
|
||||
}
|
||||
}];
|
||||
"""
|
||||
|
||||
|
||||
def build_lookup_node():
|
||||
return {
|
||||
"parameters": {
|
||||
"databaseId": BASEROW_DB,
|
||||
"tableId": MAPPING_TABLE,
|
||||
"limit": 1,
|
||||
"additionalOptions": {
|
||||
"filters": {
|
||||
"fields": [
|
||||
{
|
||||
"field": FIELD_ID_OPP_SUCURSAL,
|
||||
"value": "={{ $('Datos de Lead').item.json.Oportunidad.opportunity_id }}",
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
},
|
||||
"type": "n8n-nodes-base.baserow",
|
||||
"typeVersion": 1.1,
|
||||
"position": [4200, 1140],
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": NAME_LOOKUP,
|
||||
"credentials": BASEROW_CRED,
|
||||
"alwaysOutputData": True,
|
||||
"onError": "continueRegularOutput",
|
||||
"notes": (
|
||||
"Idempotencia global: busca en la tabla 754 el mapeo id_opp_sucursal->"
|
||||
"id_opp_marca. Si existe, 'Decidir Match' hara UPDATE de esa opp aunque "
|
||||
"el contacto resuelto sea otro. alwaysOutputData + onError para no romper "
|
||||
"el flujo si no hay match o si Baserow 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()
|
||||
client = N8NClient(*load_credentials())
|
||||
|
||||
print(f"[1/6] GET workflow {WID}")
|
||||
wf = client.get_workflow(WID)
|
||||
print(f" versionId previo: {wf.get('versionId')} | active: {wf.get('active')} | nodos: {len(wf.get('nodes') or [])}")
|
||||
|
||||
existing = {n["name"] for n in wf.get("nodes") or []}
|
||||
for needed in [SRC, NEXT, DECIDE, "Datos de Lead"]:
|
||||
if needed not in existing:
|
||||
print(f"\nERROR: falta nodo requerido {needed!r}. Abortando.")
|
||||
sys.exit(2)
|
||||
if NAME_LOOKUP in existing:
|
||||
print(f"\nERROR: nodo {NAME_LOOKUP!r} ya existe. Abortando para no duplicar.")
|
||||
sys.exit(2)
|
||||
|
||||
print(f"\n[2/6] Backup fresco")
|
||||
_, bpath = client.backup_workflow(WID, label="pre_baserow_opp_idempotency")
|
||||
print(f" backup -> {bpath}")
|
||||
|
||||
print(f"\n[3/6] Inyectar nodo lookup Baserow")
|
||||
client.add_node(wf, build_lookup_node())
|
||||
print(f" + {NAME_LOOKUP}")
|
||||
|
||||
print(f"\n[4/6] Reescribir codigo de '{DECIDE}'")
|
||||
decide = client.find_node(wf, DECIDE)
|
||||
decide["parameters"]["jsCode"] = NEW_DECIDE_CODE
|
||||
print(" jsCode actualizado (chequeo Baserow -> fallback contacto -> CREATE)")
|
||||
|
||||
print(f"\n[5/6] Reconectar grafo")
|
||||
conns = wf["connections"]
|
||||
# SRC -> LOOKUP (antes SRC -> NEXT)
|
||||
conns[SRC] = {"main": [[{"node": NAME_LOOKUP, "type": "main", "index": 0}]]}
|
||||
# LOOKUP -> NEXT
|
||||
conns[NAME_LOOKUP] = {"main": [[{"node": NEXT, "type": "main", "index": 0}]]}
|
||||
# NEXT -> DECIDE se mantiene (no se toca)
|
||||
print(f" {SRC} -> {NAME_LOOKUP} -> {NEXT} -> {DECIDE}")
|
||||
|
||||
print(f"\n[6/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
|
||||
|
||||
new_wf = client.verify_post(WID, expected_node_names=[NAME_LOOKUP], prev_version_id=wf.get("versionId"))
|
||||
print(f" versionId nuevo: {new_wf.get('versionId')}")
|
||||
if not new_wf.get("active"):
|
||||
print(" reactivando workflow...")
|
||||
client.activate(WID)
|
||||
new_wf = client.get_workflow(WID)
|
||||
print(f" active final: {new_wf.get('active')} | nodos: {len(new_wf.get('nodes') or [])}")
|
||||
print("\nOK. Workflow endurecido. Corre el test E2E para validar UPDATE vs CREATE.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user