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
+197
View File
@@ -0,0 +1,197 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""enable_duplicate_opportunity.py
Activa el ajuste `settings.allowDuplicateOpportunity = true` en una location de GHL
(por defecto la cuenta de Marca, GbKkBpCmKu2QmloKFHy3) para que un contacto pueda
tener multiples oportunidades en el mismo pipeline (necesario para replicar
multi-empeno Sucursal -> Marca).
POR QUE EXISTE:
La replicacion Sucursal->Marca de multi-empenos fallaba con
`400 "Can not create duplicate opportunity for the contact"` porque Marca tenia
el flag en false (las sucursales ya lo tienen en true). Ver memoria
opp_multiplicity_replication_gap.
TOKEN:
Escribir en /locations/{id} requiere un token de AGENCIA con scope locations.write
(el token PIT por-location del CSV da 401 en este PUT). Se lee de la variable de
entorno GHL_AGENCY_TOKEN (definida en .env, gitignored). NUNCA se imprime.
SEGURIDAD:
- El PUT envia SOLO {"settings": {"allowDuplicateOpportunity": <bool>}}; GHL hace
merge y preserva el resto de settings (crmSettings, saasSettings, etc.).
- Snapshot del settings actual en generated/migrations/ antes de mutar.
- Registro en script_audit (object_type 'location'). OJO: el rollback automatico
del dashboard (rollback_run) NO revierte settings de location -- el rollback es
manual con --disable (PUT allowDuplicateOpportunity=false) desde el snapshot.
Uso:
python scripts/enable_duplicate_opportunity.py # DRY-RUN (no escribe)
python scripts/enable_duplicate_opportunity.py --apply --run-id <uuid>
python scripts/enable_duplicate_opportunity.py --location <id> [--apply]
python scripts/enable_duplicate_opportunity.py --disable --apply --run-id <uuid> # rollback
python scripts/enable_duplicate_opportunity.py --json
"""
import argparse
import json
import os
import sys
import uuid
from datetime import datetime
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)
try:
from dotenv import load_dotenv
load_dotenv(os.path.join(ROOT_DIR, ".env"))
except Exception:
pass
import requests # noqa: E402
import script_audit # noqa: E402
from paths import MIGRATIONS_DIR # noqa: E402
BASE_URL = "https://services.leadconnectorhq.com"
BRAND_LOCATION_ID = "GbKkBpCmKu2QmloKFHy3"
SETTING_KEY = "allowDuplicateOpportunity"
def _headers(token, *, write=False):
h = {
"Authorization": f"Bearer {token}",
"Version": "2021-07-28",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0", # GHL rechaza sin User-Agent (403)
}
if write:
h["Content-Type"] = "application/json"
return h
def get_location(token, location_id):
r = requests.get(f"{BASE_URL}/locations/{location_id}", headers=_headers(token), timeout=15)
if r.status_code != 200:
raise RuntimeError(f"GET /locations/{location_id} -> HTTP {r.status_code}: {r.text[:200]}")
return r.json().get("location", {})
def put_setting(token, location_id, value):
body = {"settings": {SETTING_KEY: value}}
r = requests.put(
f"{BASE_URL}/locations/{location_id}",
headers=_headers(token, write=True),
json=body,
timeout=15,
)
if r.status_code != 200:
raise RuntimeError(f"PUT /locations/{location_id} -> HTTP {r.status_code}: {r.text[:300]}")
return r.json()
def snapshot_settings(location_id, name, settings):
os.makedirs(str(MIGRATIONS_DIR), exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
path = os.path.join(str(MIGRATIONS_DIR), f"enable_dup_opp_{location_id}_{ts}.json")
with open(path, "w", encoding="utf-8") as f:
json.dump({"location_id": location_id, "name": name, "settings_before": settings}, f,
ensure_ascii=False, indent=2)
return path
def run(location_id=BRAND_LOCATION_ID, target_value=True, dry_run=True, run_id=None, log=print):
token = os.getenv("GHL_AGENCY_TOKEN")
if not token:
raise RuntimeError("Falta GHL_AGENCY_TOKEN en el entorno (.env). Es el token de agencia con locations.write.")
log(f"[{datetime.now().strftime('%H:%M:%S')}] === enable_duplicate_opportunity (target={target_value}) ===")
log(f"Modo: {'APPLY' if not dry_run else 'DRY-RUN (no escribe)'} | location={location_id}")
loc = get_location(token, location_id)
name = loc.get("name", "?")
settings = loc.get("settings", {})
current = settings.get(SETTING_KEY, None)
log(f"Location: {name} | {SETTING_KEY} actual = {current}")
snap = snapshot_settings(location_id, name, settings)
log(f"Snapshot settings -> {snap}")
result = {
"location_id": location_id, "name": name,
"before": current, "target": target_value,
"dry_run": dry_run, "changed": False, "snapshot": snap,
}
if current is target_value:
log(f"No-op: {SETTING_KEY} ya es {target_value}.")
return result
if dry_run:
log(f"DRY-RUN: cambiaria {SETTING_KEY} {current} -> {target_value} (PUT no enviado).")
return result
change_id = None
if run_id:
change_id = script_audit.record_change(
run_id, location_id, "location", location_id,
SETTING_KEY, SETTING_KEY, {"value": current}, {"value": target_value},
)
try:
put_setting(token, location_id, target_value)
if change_id:
script_audit.mark_change(change_id, "applied")
except Exception as exc:
if change_id:
script_audit.mark_change(change_id, "failed", str(exc))
raise
# Verificacion
after = get_location(token, location_id).get("settings", {}).get(SETTING_KEY)
result["after"] = after
result["changed"] = (after is target_value)
log(f"Verificacion: {SETTING_KEY} = {after} -> {'OK' if result['changed'] else 'FALLO'}")
return result
def main():
p = argparse.ArgumentParser(description="Activa allowDuplicateOpportunity en una location de GHL.")
p.add_argument("--location", default=BRAND_LOCATION_ID, help="location_id (default: Marca).")
p.add_argument("--apply", action="store_true", help="Aplica el cambio (sin esto, dry-run).")
p.add_argument("--disable", action="store_true", help="Pone el flag en FALSE (rollback) en vez de TRUE.")
p.add_argument("--run-id", default=None, help="run_id de script_audit (recomendado con --apply).")
p.add_argument("--json", action="store_true", help="Imprime el resultado como JSON.")
args = p.parse_args()
dry_run = not args.apply
run_id = args.run_id
if args.apply and not run_id:
run_id = uuid.uuid4().hex
script_audit.create_run(run_id, "enable_duplicate_opportunity",
arguments=f"location={args.location} disable={args.disable}",
locations=[args.location])
print(f"run_id generado: {run_id}")
try:
res = run(location_id=args.location, target_value=(not args.disable),
dry_run=dry_run, run_id=run_id)
if args.apply and run_id:
script_audit.update_run_status(run_id, "completed")
except Exception as e:
print(f"ERROR: {e}")
if args.apply and run_id:
try:
script_audit.update_run_status(run_id, "failed", str(e))
except Exception:
pass
sys.exit(2)
if args.json:
print(json.dumps(res, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()