Primer commit

This commit is contained in:
2026-05-30 14:31:19 -06:00
commit a35d26fac0
277 changed files with 265240 additions and 0 deletions
+183
View File
@@ -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()