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