Primer commit
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Correct branch contacts created manually by WEB_USER to sucursal origin."""
|
||||
|
||||
import argparse
|
||||
from urllib.parse import quote
|
||||
|
||||
from tag_canal_origen_workflow import (
|
||||
contact_display_name,
|
||||
get_all_contacts,
|
||||
get_custom_field_value,
|
||||
get_schemas,
|
||||
get_opportunity,
|
||||
ghl_request,
|
||||
load_locations,
|
||||
safe_update_contact_field,
|
||||
safe_update_opportunity_field,
|
||||
script_audit,
|
||||
)
|
||||
|
||||
SUCURSAL_TAG = "sucursal"
|
||||
DIGITAL_TAGS = ["formulario", "facebook-ads"]
|
||||
WEB_USER_SOURCE = "WEB_USER"
|
||||
|
||||
CONTACT_CANAL_VALUE = "SUCURSAL"
|
||||
CONTACT_FUENTE_VALUE = "SUCURSAL"
|
||||
OPPORTUNITY_CANAL_VALUE = "Sucursal"
|
||||
OPPORTUNITY_FUENTE_VALUE = "SUCURSAL"
|
||||
LEAD_DIGITAL_VALUE = "LEAD DIGITAL"
|
||||
|
||||
|
||||
def select_branch_locations(args):
|
||||
branches = load_locations(include_main=False)
|
||||
if args.location:
|
||||
matches = [acc for acc in branches if acc["location_id"] == args.location]
|
||||
if not matches:
|
||||
raise SystemExit(f"Location {args.location} is not a branch location in the CSV")
|
||||
return matches
|
||||
if args.all:
|
||||
return branches
|
||||
raise SystemExit("Specify --location <branch-location-id> or --all. Use --dry-run to preview without changes.")
|
||||
|
||||
|
||||
def is_web_user_contact(contact):
|
||||
created_by = contact.get("createdBy") or {}
|
||||
return created_by.get("source") == WEB_USER_SOURCE
|
||||
|
||||
|
||||
def normalized_tags(contact):
|
||||
return {str(tag).lower(): str(tag) for tag in contact.get("tags", []) or []}
|
||||
|
||||
|
||||
def update_contact_tags(contact, tags):
|
||||
contact["tags"] = tags
|
||||
|
||||
|
||||
def add_contact_tag(contact_id, tag, token):
|
||||
ghl_request("POST", f"/contacts/{contact_id}/tags", token, json_body={"tags": [tag]})
|
||||
|
||||
|
||||
def remove_contact_tag(location_id, contact_id, tag, token):
|
||||
encoded_tag = quote(tag, safe="")
|
||||
ghl_request("DELETE", f"/contacts/{contact_id}/tags/{encoded_tag}", token, params={"locationId": location_id})
|
||||
|
||||
|
||||
def safe_add_contact_tag(run_id, location_id, contact, tag, token, dry_run=False):
|
||||
tag_map = normalized_tags(contact)
|
||||
if tag.lower() in tag_map:
|
||||
return False
|
||||
|
||||
old_tags = list(contact.get("tags", []) or [])
|
||||
new_tags = old_tags + [tag]
|
||||
change_id = script_audit.record_change(run_id, location_id, "contact", contact["id"], f"tag:{tag}", "tags", old_tags, new_tags)
|
||||
if dry_run:
|
||||
update_contact_tags(contact, new_tags)
|
||||
return True
|
||||
|
||||
try:
|
||||
add_contact_tag(contact["id"], tag, token)
|
||||
script_audit.mark_change(change_id, "applied")
|
||||
update_contact_tags(contact, new_tags)
|
||||
return True
|
||||
except Exception as exc:
|
||||
script_audit.mark_change(change_id, "failed", str(exc))
|
||||
raise
|
||||
|
||||
|
||||
def safe_remove_contact_tag(run_id, location_id, contact, tag, token, dry_run=False):
|
||||
tag_map = normalized_tags(contact)
|
||||
existing_tag = tag_map.get(tag.lower())
|
||||
if not existing_tag:
|
||||
return False
|
||||
|
||||
old_tags = list(contact.get("tags", []) or [])
|
||||
new_tags = [current for current in old_tags if str(current).lower() != tag.lower()]
|
||||
change_id = script_audit.record_change(run_id, location_id, "contact", contact["id"], f"tag:{tag}", "tags", old_tags, new_tags)
|
||||
if dry_run:
|
||||
update_contact_tags(contact, new_tags)
|
||||
return True
|
||||
|
||||
try:
|
||||
remove_contact_tag(location_id, contact["id"], existing_tag, token)
|
||||
script_audit.mark_change(change_id, "applied")
|
||||
update_contact_tags(contact, new_tags)
|
||||
return True
|
||||
except Exception as exc:
|
||||
script_audit.mark_change(change_id, "failed", str(exc))
|
||||
raise
|
||||
|
||||
|
||||
def get_contact_opportunities(location_id, contact_id, token):
|
||||
try:
|
||||
data = ghl_request(
|
||||
"POST",
|
||||
"/opportunities/search",
|
||||
token,
|
||||
json_body={"locationId": location_id, "contactId": contact_id, "limit": 100},
|
||||
)
|
||||
except Exception as exc:
|
||||
print(f" WARN no se pudieron consultar oportunidades: {exc}")
|
||||
return []
|
||||
return data.get("opportunities", []) or []
|
||||
|
||||
|
||||
def oldest_opportunity(opportunities):
|
||||
if not opportunities:
|
||||
return None
|
||||
return sorted(opportunities, key=lambda opp: opp.get("dateAdded") or "9999-12-31T23:59:59Z")[0]
|
||||
|
||||
|
||||
def update_oldest_opportunity(run_id, location_id, contact, opportunity_schema, token, dry_run=False):
|
||||
opportunities = get_contact_opportunities(location_id, contact["id"], token)
|
||||
oldest = oldest_opportunity(opportunities)
|
||||
if not oldest:
|
||||
return False
|
||||
|
||||
opportunity_id = oldest.get("id")
|
||||
opportunity = get_opportunity(location_id, opportunity_id, token) or oldest
|
||||
updated = False
|
||||
|
||||
field_updates = [
|
||||
("Canal de Origen de la Oportunidad", OPPORTUNITY_CANAL_VALUE, "always"),
|
||||
("Fuente de Prospecto", OPPORTUNITY_FUENTE_VALUE, "always"),
|
||||
("Fuente del cliente potencial", OPPORTUNITY_FUENTE_VALUE, "lead_digital_only"),
|
||||
]
|
||||
|
||||
for field_name, value, mode in field_updates:
|
||||
field_id = opportunity_schema.get(field_name)
|
||||
if not field_id:
|
||||
continue
|
||||
current_value = get_custom_field_value(opportunity, field_id)
|
||||
if mode == "lead_digital_only" and current_value != LEAD_DIGITAL_VALUE:
|
||||
continue
|
||||
if safe_update_opportunity_field(run_id, location_id, opportunity_id, opportunity, field_id, field_name, value, token, dry_run):
|
||||
updated = True
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
def process_location(account, dry_run=False, run_id=None):
|
||||
location_id = account["location_id"]
|
||||
token = account["token"]
|
||||
name = account["nombre"]
|
||||
|
||||
schemas = get_schemas(location_id, token, "contact", "opportunity")
|
||||
contact_schema = schemas["contact"]
|
||||
opportunity_schema = schemas["opportunity"]
|
||||
contact_canal_id = contact_schema.get("Canal de Origen")
|
||||
contact_fuente_id = contact_schema.get("Fuente de Prospecto")
|
||||
|
||||
if not contact_canal_id or not contact_fuente_id:
|
||||
print(
|
||||
f"\n{name} - missing required contact fields "
|
||||
f"(Canal: {bool(contact_canal_id)}, Fuente: {bool(contact_fuente_id)})"
|
||||
)
|
||||
return 0
|
||||
|
||||
contacts = get_all_contacts(location_id, token)
|
||||
web_user_contacts = 0
|
||||
corrected_contacts = 0
|
||||
updated_opportunities = 0
|
||||
skipped_opportunities = 0
|
||||
|
||||
for contact in contacts:
|
||||
if not script_audit.wait_if_paused_or_stopped(run_id):
|
||||
print("\nDetención segura solicitada. Saliendo antes del siguiente contacto.")
|
||||
break
|
||||
if not is_web_user_contact(contact):
|
||||
continue
|
||||
|
||||
web_user_contacts += 1
|
||||
contact_changed = False
|
||||
|
||||
if safe_add_contact_tag(run_id, location_id, contact, SUCURSAL_TAG, token, dry_run):
|
||||
contact_changed = True
|
||||
for tag in DIGITAL_TAGS:
|
||||
if safe_remove_contact_tag(run_id, location_id, contact, tag, token, dry_run):
|
||||
contact_changed = True
|
||||
|
||||
if safe_update_contact_field(
|
||||
run_id,
|
||||
location_id,
|
||||
contact,
|
||||
contact_canal_id,
|
||||
"Canal de Origen",
|
||||
CONTACT_CANAL_VALUE,
|
||||
token,
|
||||
dry_run,
|
||||
):
|
||||
contact_changed = True
|
||||
|
||||
if safe_update_contact_field(
|
||||
run_id,
|
||||
location_id,
|
||||
contact,
|
||||
contact_fuente_id,
|
||||
"Fuente de Prospecto",
|
||||
CONTACT_FUENTE_VALUE,
|
||||
token,
|
||||
dry_run,
|
||||
):
|
||||
contact_changed = True
|
||||
|
||||
if not contact_changed:
|
||||
continue
|
||||
|
||||
corrected_contacts += 1
|
||||
print(f" OK contacto {contact_display_name(contact)} | WEB_USER -> sucursal")
|
||||
|
||||
if update_oldest_opportunity(run_id, location_id, contact, opportunity_schema, token, dry_run):
|
||||
updated_opportunities += 1
|
||||
print(" OK oportunidad mas antigua actualizada")
|
||||
else:
|
||||
skipped_opportunities += 1
|
||||
|
||||
print(
|
||||
f"\n{name}: {web_user_contacts} WEB_USER detectados, "
|
||||
f"{corrected_contacts} contactos corregidos, "
|
||||
f"{updated_opportunities} oportunidades actualizadas, "
|
||||
f"{skipped_opportunities} sin cambio de oportunidad"
|
||||
)
|
||||
return corrected_contacts
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Correct WEB_USER branch contacts to sucursal origin")
|
||||
parser.add_argument("--location", help="Specific branch location ID to process")
|
||||
parser.add_argument("--all", action="store_true", help="Process all branch locations from the CSV")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing to GHL")
|
||||
parser.add_argument("--run-id", help="Audit run ID supplied by the dashboard")
|
||||
args = parser.parse_args()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("GHL FIX - WEB_USER BRANCH CONTACTS -> SUCURSAL")
|
||||
print("=" * 60)
|
||||
if args.dry_run:
|
||||
print("DRY RUN - no changes will be made\n")
|
||||
|
||||
total = 0
|
||||
for account in select_branch_locations(args):
|
||||
try:
|
||||
total += process_location(account, args.dry_run, args.run_id)
|
||||
except Exception as exc:
|
||||
print(f"\nERROR {account['nombre']}: {exc}")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"TOTAL: {total} contactos WEB_USER corregidos")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user