Primer commit
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env python3
|
||||
"""One-off cleanup para los hallazgos residuales en Cancún y Uruapan.
|
||||
|
||||
Cancún `opportunity.vehiculo` vs `opportunity.opportunityvehiculo`:
|
||||
4 records con valores complementarios (no contradictorios). Resolución:
|
||||
- Josué Alcocer y Rafael Reyes: el contact tiene la fuente canónica con
|
||||
marca+version+año completos. PUT opp.vehiculo = string canónico desde
|
||||
el contact.
|
||||
- ismael flores y Angel Juarez: contact vacío. Concatenar valores del
|
||||
orphan + clean en formato Marca Modelo Año.
|
||||
Después: DELETE orphan `opportunity.opportunityvehiculo`.
|
||||
|
||||
Uruapan `contact.fuente_del_prospecto`:
|
||||
1 record con orphan='Sucursal' vs clean='SUCURSAL'. Mismo valor lógico
|
||||
con casing distinto. Clean ya está alineado con Marca.
|
||||
Acción: DELETE orphan field (zombie en gabriela ramos queda invisible).
|
||||
|
||||
Default DRY-RUN. Usar --apply para escritura real.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore", message=r"urllib3 .* doesn't match a supported version!")
|
||||
import requests
|
||||
|
||||
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)
|
||||
|
||||
import sync_engine # noqa: E402
|
||||
|
||||
BASE_URL = "https://services.leadconnectorhq.com"
|
||||
API_VERSION = "2021-07-28"
|
||||
from paths import MIGRATIONS_DIR # noqa: E402
|
||||
|
||||
CANCUN_LOC = "uJEn2iuUficuml9zxAnt"
|
||||
URUAPAN_LOC = "FoQWuksh4wQjPbVVZ8ZQ"
|
||||
|
||||
# Valores finales decididos por record en Cancún.
|
||||
CANCUN_OPP_VEHICULO_UPDATES = [
|
||||
{"opp_id": "hGghAwTK9vLoI7aRNED4", "label": "Josué Alcocer",
|
||||
"new_value": "Honda Brv Mas equipada 2018",
|
||||
"source": "regenerated_from_contact"},
|
||||
{"opp_id": "xiLM8vGogPWNHt9YljPC", "label": "Rafael Reyes Hernandez",
|
||||
"new_value": "Mercedes benz Cla 200 2019",
|
||||
"source": "regenerated_from_contact"},
|
||||
{"opp_id": "bdFbVzaBr2RJic2rZwDj", "label": "ismael flores",
|
||||
"new_value": "BEAT 2018",
|
||||
"source": "concat_orphan_clean"},
|
||||
{"opp_id": "wy4Y0QvVn48QN3iHcC3m", "label": "Angel Juarez Lopez",
|
||||
"new_value": "Nissan VERSSA 2025",
|
||||
"source": "concat_orphan_clean_normalized_tab"},
|
||||
]
|
||||
|
||||
CANCUN_ORPHAN_FIELDKEY = "opportunity.opportunityvehiculo"
|
||||
CANCUN_CLEAN_FIELDKEY = "opportunity.vehiculo"
|
||||
|
||||
URUAPAN_ORPHAN_FIELDKEY = "contact.fuente_del_prospecto"
|
||||
|
||||
|
||||
_last_request_by_token: dict = {}
|
||||
|
||||
|
||||
def wait_for_rate_limit(token):
|
||||
now = time.time()
|
||||
elapsed = now - _last_request_by_token.get(token, 0)
|
||||
if elapsed < 0.110:
|
||||
time.sleep(0.110 - elapsed)
|
||||
_last_request_by_token[token] = time.time()
|
||||
|
||||
|
||||
def ghl_request(method, endpoint, token, *, params=None, json_body=None, version=API_VERSION):
|
||||
wait_for_rate_limit(token)
|
||||
url = endpoint if endpoint.startswith("http") else f"{BASE_URL}{endpoint}"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Version": version,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
resp = requests.request(method, url, headers=headers, params=params, json=json_body, timeout=45)
|
||||
resp.raise_for_status()
|
||||
if resp.status_code == 204:
|
||||
return {}
|
||||
return resp.json()
|
||||
|
||||
|
||||
def cf_value(record, field_id):
|
||||
for f in record.get("customFields", []) or []:
|
||||
if f.get("id") != field_id:
|
||||
continue
|
||||
for key in ("value", "fieldValue", "fieldValueString"):
|
||||
v = f.get(key)
|
||||
if v is not None:
|
||||
return v
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def find_field_id(location_id, token, fieldkey):
|
||||
for model in ("contact", "opportunity"):
|
||||
data = ghl_request("GET", f"/locations/{location_id}/customFields", token,
|
||||
params={"model": model})
|
||||
for cf in data.get("customFields") or []:
|
||||
if cf.get("fieldKey") == fieldkey:
|
||||
return cf.get("id")
|
||||
return None
|
||||
|
||||
|
||||
def write_snapshot(payload, name):
|
||||
os.makedirs(MIGRATIONS_DIR, exist_ok=True)
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe = "".join(c if c.isalnum() else "_" for c in name)[:60]
|
||||
path = os.path.join(MIGRATIONS_DIR, f"residual_{safe}_{ts}.json")
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(payload, fh, ensure_ascii=False, indent=2, default=str)
|
||||
return path
|
||||
|
||||
|
||||
def fix_cancun(branch, apply_flag):
|
||||
location_id = branch["location_id"]
|
||||
token = branch["token"]
|
||||
print(f"\n=== CANCÚN ({location_id}) ===")
|
||||
clean_id = find_field_id(location_id, token, CANCUN_CLEAN_FIELDKEY)
|
||||
orphan_id = find_field_id(location_id, token, CANCUN_ORPHAN_FIELDKEY)
|
||||
print(f" clean_field {CANCUN_CLEAN_FIELDKEY} → id={clean_id}")
|
||||
print(f" orphan_field {CANCUN_ORPHAN_FIELDKEY} → id={orphan_id}")
|
||||
if not clean_id:
|
||||
print(" ✗ clean field no existe en Cancún. ABORTAR.")
|
||||
return
|
||||
|
||||
# Snapshot del estado actual de cada opp + valor nuevo a aplicar
|
||||
snapshot_records = []
|
||||
for upd in CANCUN_OPP_VEHICULO_UPDATES:
|
||||
ro = ghl_request("GET", f"/opportunities/{upd['opp_id']}", token,
|
||||
params={"locationId": location_id})
|
||||
opp = ro.get("opportunity", ro)
|
||||
snapshot_records.append({
|
||||
"opp_id": upd["opp_id"],
|
||||
"label": upd["label"],
|
||||
"contact_id": opp.get("contactId"),
|
||||
"current_clean": cf_value(opp, clean_id),
|
||||
"current_orphan": cf_value(opp, orphan_id) if orphan_id else None,
|
||||
"new_value": upd["new_value"],
|
||||
"source": upd["source"],
|
||||
})
|
||||
snap_path = write_snapshot({
|
||||
"branch": branch["nombre"],
|
||||
"location_id": location_id,
|
||||
"operation": "cancun_vehiculo_merge",
|
||||
"clean_field_id": clean_id,
|
||||
"orphan_field_id": orphan_id,
|
||||
"records": snapshot_records,
|
||||
"apply": apply_flag,
|
||||
"timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
}, "cancun_vehiculo")
|
||||
print(f" Snapshot → {snap_path}")
|
||||
|
||||
print()
|
||||
print(" Plan:")
|
||||
for rec in snapshot_records:
|
||||
print(f" {rec['label']:30s} | actual='{rec['current_clean']}' → nuevo='{rec['new_value']}' ({rec['source']})")
|
||||
|
||||
if not apply_flag:
|
||||
print(f"\n [DRY-RUN] No se aplican cambios.")
|
||||
return
|
||||
|
||||
# PUT cada opp con el nuevo valor en clean field
|
||||
errors = []
|
||||
updated = 0
|
||||
for rec in snapshot_records:
|
||||
try:
|
||||
ghl_request("PUT", f"/opportunities/{rec['opp_id']}", token,
|
||||
json_body={"customFields": [{"id": clean_id, "value": rec["new_value"]}]})
|
||||
updated += 1
|
||||
print(f" ✓ PUT opp {rec['opp_id']} ({rec['label']}) ← '{rec['new_value']}'")
|
||||
except Exception as exc:
|
||||
errors.append({"opp_id": rec["opp_id"], "error": str(exc)})
|
||||
print(f" ✗ PUT falló opp {rec['opp_id']}: {exc}")
|
||||
|
||||
if errors:
|
||||
print(f"\n ✗ {len(errors)} PUTs fallaron. NO se elimina el orphan field. Snapshot preservado.")
|
||||
return
|
||||
|
||||
if orphan_id:
|
||||
try:
|
||||
ghl_request("DELETE", f"/locations/{location_id}/customFields/{orphan_id}", token)
|
||||
print(f"\n ✓ DELETE orphan field {orphan_id} ({CANCUN_ORPHAN_FIELDKEY})")
|
||||
except Exception as exc:
|
||||
print(f"\n ⚠ DELETE orphan falló: {exc}")
|
||||
|
||||
|
||||
def fix_uruapan(branch, apply_flag):
|
||||
location_id = branch["location_id"]
|
||||
token = branch["token"]
|
||||
print(f"\n=== URUAPAN ({location_id}) ===")
|
||||
orphan_id = find_field_id(location_id, token, URUAPAN_ORPHAN_FIELDKEY)
|
||||
print(f" orphan_field {URUAPAN_ORPHAN_FIELDKEY} → id={orphan_id}")
|
||||
if not orphan_id:
|
||||
print(f" ✓ orphan ya no existe. SKIP.")
|
||||
return
|
||||
|
||||
snap_path = write_snapshot({
|
||||
"branch": branch["nombre"],
|
||||
"location_id": location_id,
|
||||
"operation": "uruapan_orphan_delete",
|
||||
"orphan_field_id": orphan_id,
|
||||
"orphan_fieldkey": URUAPAN_ORPHAN_FIELDKEY,
|
||||
"reason": "Valor 'Sucursal' duplicado funcionalmente como 'SUCURSAL' en contact.fuente_de_prospecto. Clean ya alineado con Marca.",
|
||||
"apply": apply_flag,
|
||||
"timestamp_utc": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
}, "uruapan_fuente_del_prospecto")
|
||||
print(f" Snapshot → {snap_path}")
|
||||
print(f" Plan: DELETE orphan {orphan_id}. Zombie value en gabriela ramos queda invisible.")
|
||||
|
||||
if not apply_flag:
|
||||
print(f"\n [DRY-RUN] No se aplica.")
|
||||
return
|
||||
|
||||
try:
|
||||
ghl_request("DELETE", f"/locations/{location_id}/customFields/{orphan_id}", token)
|
||||
print(f" ✓ DELETE orphan field {orphan_id}")
|
||||
except Exception as exc:
|
||||
print(f" ⚠ DELETE orphan falló: {exc}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="One-off cleanup Cancún + Uruapan.")
|
||||
parser.add_argument("--apply", action="store_true",
|
||||
help="Aplica cambios reales. Sin esto corre en DRY-RUN.")
|
||||
parser.add_argument("--cancun-only", action="store_true",
|
||||
help="Solo procesa Cancún.")
|
||||
parser.add_argument("--uruapan-only", action="store_true",
|
||||
help="Solo procesa Uruapan.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
accounts = sync_engine.parse_accounts_csv()
|
||||
accounts_by_id = {a["location_id"]: a for a in accounts}
|
||||
|
||||
print("=" * 70)
|
||||
print(f"Modo: {'APPLY (escritura real)' if args.apply else 'DRY-RUN'}")
|
||||
print("=" * 70)
|
||||
|
||||
if not args.uruapan_only:
|
||||
cancun = accounts_by_id.get(CANCUN_LOC)
|
||||
if cancun:
|
||||
fix_cancun(cancun, args.apply)
|
||||
else:
|
||||
print(f"\n⚠ Cancún ({CANCUN_LOC}) no en CSV.")
|
||||
|
||||
if not args.cancun_only:
|
||||
uruapan = accounts_by_id.get(URUAPAN_LOC)
|
||||
if uruapan:
|
||||
fix_uruapan(uruapan, args.apply)
|
||||
else:
|
||||
print(f"\n⚠ Uruapan ({URUAPAN_LOC}) no en CSV.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user