Primer commit
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Cablea el UPSERT en tiempo real del mapeo Baserow (tabla 754) tras crear/
|
||||
actualizar la opp en Marca, en el workflow Cfgwp0bOtDW8zuKW.
|
||||
|
||||
Contexto: el lookup Baserow + 'Decidir Match' ya evitan duplicados cuando el
|
||||
mapeo id_opp_sucursal->id_opp_marca existe. PERO las salidas de
|
||||
'Crear Oportunidad - MARCA' y 'Actualizar Oportunidad - MARCA (v2)' estaban
|
||||
VACIAS -> el mapeo solo se refrescaba con el backfill agendado (cada 30 min).
|
||||
En esa ventana, una re-ejecucion del mismo opp podia volver a no encontrar el
|
||||
mapeo y, si el contacto era ambiguo, crear un duplicado.
|
||||
|
||||
Fix (aditivo, 2 nodos, create-only condicional -> minimo riesgo):
|
||||
- 'Preparar Upsert Mapeo' [code]: decide si hay que escribir el mapeo. Solo
|
||||
escribe cuando NO existe aun (action CREATE, o UPDATE resuelto por contacto;
|
||||
NUNCA cuando 'Decidir Match' ya lo resolvio via Baserow -> la fila ya existe,
|
||||
no duplicar). Calcula id_opp_marca segun el camino. Si no hace falta -> [].
|
||||
- 'Crear Mapeo - Baserow' [baserow create]: inserta la fila {id_opp_sucursal,
|
||||
id_opp_marca, location_id_sucursal, updated_at}.
|
||||
- Ambos con onError=continueRegularOutput: si Baserow falla, NO rompe la
|
||||
replicacion (la opp ya quedo creada/actualizada aguas arriba; el backfill
|
||||
completara el mapeo despues).
|
||||
|
||||
Idempotente: el backfill verifica por id_opp_sucursal antes de crear, asi que
|
||||
una fila creada por este upsert no se duplica luego.
|
||||
|
||||
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
|
||||
F_ID_OPP_SUCURSAL = 7280
|
||||
F_ID_OPP_MARCA = 7283
|
||||
F_LOCATION_SUCURSAL = 7284
|
||||
F_UPDATED_AT = 7285
|
||||
BASEROW_CRED = {"baserowApi": {"id": "LZztQ3WMpzXjSTIH", "name": "Baserow account"}}
|
||||
|
||||
NAME_PREP = "Preparar Upsert Mapeo"
|
||||
NAME_CREATE = "Crear Mapeo - Baserow"
|
||||
CREATE_SRC = "Crear Oportunidad - MARCA"
|
||||
UPDATE_SRC = "Actualizar Oportunidad - MARCA (v2)"
|
||||
DECIDE = "Decidir Match (Create vs Update)"
|
||||
|
||||
PREP_CODE = """// Prepara el UPSERT del mapeo Baserow (tabla 754) tras CREATE/UPDATE de la opp en Marca.
|
||||
// Solo escribe cuando el mapeo NO existe aun:
|
||||
// - action CREATE -> la fila no existe (el lookup Baserow fallo) -> crear.
|
||||
// - action UPDATE por contacto -> Baserow no la tenia -> crear (idempotencia futura).
|
||||
// - action UPDATE por Baserow -> la fila YA existe -> NO escribir (no duplicar).
|
||||
// Asi una re-ejecucion del mismo opp encuentra el mapeo y hace UPDATE, no CREATE duplicado.
|
||||
const decision = $('Decidir Match (Create vs Update)').first().json;
|
||||
const action = decision.action;
|
||||
const matchReason = decision.matchReason || '';
|
||||
const sucursalOppId = decision.sucursalOppId;
|
||||
|
||||
const matchedByBaserow = matchReason.indexOf('Baserow') !== -1;
|
||||
const needWrite = (action === 'CREATE') || (action === 'UPDATE' && !matchedByBaserow);
|
||||
if (!needWrite) return [];
|
||||
|
||||
// id de la opp en Marca segun el camino
|
||||
let marcaOppId = decision.opportunityId;
|
||||
if (action === 'CREATE') {
|
||||
try {
|
||||
const co = $('Crear Oportunidad - MARCA').first().json;
|
||||
marcaOppId = (co.opportunity && co.opportunity.id) || co.id || marcaOppId;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!marcaOppId || !sucursalOppId) return [];
|
||||
|
||||
let locationSucursal = '';
|
||||
try { locationSucursal = $('DATOS API - SUCURSAL').first().json['Location ID'] || ''; } catch (e) {}
|
||||
|
||||
return [{ json: { sucursalOppId, marcaOppId, locationSucursal } }];
|
||||
"""
|
||||
|
||||
|
||||
def build_prep_node():
|
||||
return {
|
||||
"parameters": {"jsCode": PREP_CODE},
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [6040, 960],
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": NAME_PREP,
|
||||
"onError": "continueRegularOutput",
|
||||
"notes": ("Decide si escribir el mapeo Baserow tras CREATE/UPDATE. Solo cuando la "
|
||||
"fila no existe (CREATE o UPDATE-por-contacto). onError continue para no "
|
||||
"romper la replicacion."),
|
||||
}
|
||||
|
||||
|
||||
def build_create_node():
|
||||
return {
|
||||
"parameters": {
|
||||
"operation": "create",
|
||||
"databaseId": BASEROW_DB,
|
||||
"tableId": MAPPING_TABLE,
|
||||
"fieldsUi": {
|
||||
"fieldValues": [
|
||||
{"fieldId": F_ID_OPP_SUCURSAL, "fieldValue": "={{ $json.sucursalOppId }}"},
|
||||
{"fieldId": F_ID_OPP_MARCA, "fieldValue": "={{ $json.marcaOppId }}"},
|
||||
{"fieldId": F_LOCATION_SUCURSAL, "fieldValue": "={{ $json.locationSucursal }}"},
|
||||
{"fieldId": F_UPDATED_AT, "fieldValue": "={{ $now.toISO() }}"},
|
||||
]
|
||||
},
|
||||
},
|
||||
"type": "n8n-nodes-base.baserow",
|
||||
"typeVersion": 1,
|
||||
"position": [6280, 960],
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": NAME_CREATE,
|
||||
"credentials": BASEROW_CRED,
|
||||
"onError": "continueRegularOutput",
|
||||
"notes": ("Inserta el mapeo id_opp_sucursal->id_opp_marca en la tabla 754 en tiempo "
|
||||
"real. Idempotente con el backfill (verifica por id_opp_sucursal)."),
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--apply", action="store_true", help="PUT real (default: dry-run).")
|
||||
args = ap.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 [CREATE_SRC, UPDATE_SRC, DECIDE]:
|
||||
if needed not in existing:
|
||||
print(f"\nERROR: falta nodo requerido {needed!r}. Abortando.")
|
||||
sys.exit(2)
|
||||
for new in [NAME_PREP, NAME_CREATE]:
|
||||
if new in existing:
|
||||
print(f"\nERROR: nodo {new!r} ya existe. Abortando para no duplicar.")
|
||||
sys.exit(2)
|
||||
|
||||
print("\n[2/6] Backup fresco")
|
||||
_, bpath = client.backup_workflow(WID, label="pre_baserow_opp_upsert")
|
||||
print(f" backup -> {bpath}")
|
||||
|
||||
print("\n[3/6] Inyectar nodos")
|
||||
client.add_node(wf, build_prep_node())
|
||||
client.add_node(wf, build_create_node())
|
||||
print(f" + {NAME_PREP}\n + {NAME_CREATE}")
|
||||
|
||||
print("\n[4/6] Reconectar grafo (aditivo; las salidas estaban vacias)")
|
||||
conns = wf["connections"]
|
||||
conns[CREATE_SRC] = {"main": [[{"node": NAME_PREP, "type": "main", "index": 0}]]}
|
||||
conns[UPDATE_SRC] = {"main": [[{"node": NAME_PREP, "type": "main", "index": 0}]]}
|
||||
conns[NAME_PREP] = {"main": [[{"node": NAME_CREATE, "type": "main", "index": 0}]]}
|
||||
print(f" {CREATE_SRC} -> {NAME_PREP}")
|
||||
print(f" {UPDATE_SRC} -> {NAME_PREP}")
|
||||
print(f" {NAME_PREP} -> {NAME_CREATE}")
|
||||
|
||||
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("\n[6/6] Verificar")
|
||||
new_wf = client.verify_post(WID, expected_node_names=[NAME_PREP, NAME_CREATE],
|
||||
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. Upsert Baserow en tiempo real cableado (degradacion elegante via onError).")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user