descripción

This commit is contained in:
2026-05-30 20:16:12 -06:00
parent a35d26fac0
commit fb20cf8bd5
23 changed files with 32388 additions and 2 deletions
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Read-only: clasifica la incoherencia Canal de Origen / Fuente de Prospecto.
Detecta en SUCURSALES los dos patrones incoherentes y, para cada contacto,
lee ``createdBy.source`` EN VIVO (solo viene en el GET individual) para decidir
la verdad del lead:
Patron A: Canal=SUCURSAL & Fuente=LEAD DIGITAL (viola AGENTS Cap.3)
- createdBy in {WEB_USER, MOBILE_USER} -> manual sucursal -> arreglar FUENTE (->SUCURSAL)
- otro (INTEGRATION/form/etc.) -> lead digital -> arreglar CANAL (->FORMULARIO/FACEBOOK)
Patron B: Fuente=REDES SOCIALES -> lead digital -> Canal=FACEBOOK + Fuente=LEAD DIGITAL
No escribe nada. Imprime la particion y el plan por contacto.
"""
import argparse
import json
import os
import sqlite3
import sys
from collections import Counter
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 paths # noqa: E402
from tag_canal_origen_workflow import ( # noqa: E402
MAIN_LOCATION_ID,
contact_display_name,
ghl_request,
load_locations,
)
from canal_origen_resolver import classify_source # noqa: E402
USER_SOURCES = {"WEB_USER", "MOBILE_USER"}
def cfval(custom_fields, fid):
for f in custom_fields or []:
if f.get("id") == fid:
for k in ("value", "fieldValue", "fieldValueString"):
if f.get(k) is not None:
return f[k]
return None
def field_maps(conn, location_id):
m = {}
for r in conn.execute(
"select field_id, field_name from object_schemas where location_id=? and object_key='contact'",
(location_id,),
):
m[r[1].strip().lower()] = r[0]
return m
def incoherent_from_cache(conn, location_id):
"""Devuelve [(contact_id, patron, canal, fuente)] desde la cache."""
fm = field_maps(conn, location_id)
canal_id = fm.get("canal de origen")
fuente_id = fm.get("fuente de prospecto")
out = []
for r in conn.execute(
"select id, custom_fields_json from contacts where location_id=?",
(location_id,),
):
cf = json.loads(r[1] or "[]")
canal = cfval(cf, canal_id)
fuente = cfval(cf, fuente_id)
if canal == "SUCURSAL" and fuente == "LEAD DIGITAL":
out.append((r[0], "A", canal, fuente))
elif fuente == "REDES SOCIALES":
out.append((r[0], "B", canal, fuente))
return out
def get_contact_full(contact_id, token):
data = ghl_request("GET", f"/contacts/{contact_id}", token)
inner = data.get("contact")
return inner if isinstance(inner, dict) else data
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Audit read-only de incoherencia origen/fuente")
parser.add_argument("--all", action="store_true", help="Todas las sucursales productivas")
parser.add_argument("--location", help="Una location especifica")
args = parser.parse_args()
accounts = load_locations(include_main=False)
accounts = [a for a in accounts if a["location_id"] != MAIN_LOCATION_ID]
if args.location:
accounts = [a for a in accounts if a["location_id"] == args.location]
elif not args.all:
raise SystemExit("Usa --all o --location <id>")
conn = sqlite3.connect(paths.DB_PATH)
grand = Counter()
for acc in accounts:
loc = acc["location_id"]
token = acc["token"]
targets = incoherent_from_cache(conn, loc)
if not targets:
continue
print(f"\n{'='*70}\n{acc['nombre']} ({loc}) - {len(targets)} incoherentes\n{'='*70}")
for cid, patron, canal, fuente in targets:
full = get_contact_full(cid, token)
created = (full.get("createdBy") or {}).get("source")
src = full.get("source")
name = contact_display_name(full)
if patron == "B":
plan = "Canal->FACEBOOK + Fuente->LEAD DIGITAL"
bucket = "B_redes->digital"
else:
if created in USER_SOURCES:
plan = "Fuente->SUCURSAL (manual sucursal)"
bucket = "A_manual->fuente"
else:
# createdBy no-usuario => digital; canal segun source
src_tag = classify_source(src)
canal_target = "FACEBOOK" if src_tag == "facebook-ads" else "FORMULARIO"
plan = f"Canal->{canal_target} (digital)"
bucket = f"A_digital->canal({canal_target})"
grand[bucket] += 1
print(f" {name:35.35} | createdBy={created or '-':12} source={src or '-':12} | {plan}")
print(f"\n{'='*70}\nRESUMEN GLOBAL (plan)\n{'='*70}")
for k, v in grand.most_common():
print(f" {v:4} {k}")
if __name__ == "__main__":
main()