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
+270
View File
@@ -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()