Primer commit
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user