Primer commit
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
"""Resolver de Canal de Origen + tag canonico para un contacto.
|
||||
|
||||
Combina señales (source nativo de GHL, tags existentes, Canal de Origen previo,
|
||||
Fuente de Prospecto) para producir UNA decisión consistente:
|
||||
|
||||
- `decided_tag`: tag canonico que debe quedar (facebook-ads | sucursal | formulario).
|
||||
- `canal_value`: valor para custom field "Canal de Origen" (FACEBOOK | SUCURSAL | FORMULARIO).
|
||||
- `tags_to_add` / `tags_to_remove`: ajustes necesarios sobre las etiquetas actuales.
|
||||
- `confident` / `review`: si la decisión es segura o requiere revisión humana.
|
||||
- `reason`: explicación textual para auditoría.
|
||||
|
||||
Reglas de prioridad (de mayor a menor):
|
||||
1. `source` nativo de GHL si es interpretable.
|
||||
2. Único tag target ya presente, salvo que source Y canal previo lo contradigan.
|
||||
3. Canal de Origen previo si existe y es del catálogo.
|
||||
4. Fuente de Prospecto (SUCURSAL fuerza sucursal; LEAD DIGITAL favorece formulario sobre facebook salvo evidencia).
|
||||
"""
|
||||
|
||||
TARGET_TAGS = ("facebook-ads", "sucursal", "formulario")
|
||||
|
||||
CANAL_BY_TAG = {
|
||||
"facebook-ads": "FACEBOOK",
|
||||
"sucursal": "SUCURSAL",
|
||||
"formulario": "FORMULARIO",
|
||||
}
|
||||
|
||||
TAG_BY_CANAL = {v: k for k, v in CANAL_BY_TAG.items()}
|
||||
|
||||
FUENTE_BY_TAG = {
|
||||
"facebook-ads": "LEAD DIGITAL",
|
||||
"sucursal": "SUCURSAL",
|
||||
"formulario": "LEAD DIGITAL",
|
||||
}
|
||||
|
||||
DIGITAL_KEYWORDS = ("facebook", "ads", "instagram", "fb.com", "ig ", " ig", "ig:", "meta")
|
||||
FORM_KEYWORDS = (
|
||||
"form", "formulario", "web", "landing", "page", "survey", "encuesta",
|
||||
"qr", "chat", "widget", "inbound", "tiktok", "whatsapp", "wa.me", "api",
|
||||
"sms", "lead-form", "leadform",
|
||||
)
|
||||
SUCURSAL_KEYWORDS = ("sucursal", "manual", "tienda", "offline", "store", "walk-in", "walkin")
|
||||
|
||||
|
||||
def classify_source(source):
|
||||
s = (source or "").strip().lower()
|
||||
if not s:
|
||||
return None
|
||||
if any(w in s for w in DIGITAL_KEYWORDS):
|
||||
return "facebook-ads"
|
||||
if any(w in s for w in FORM_KEYWORDS):
|
||||
return "formulario"
|
||||
if any(w in s for w in SUCURSAL_KEYWORDS):
|
||||
return "sucursal"
|
||||
return None
|
||||
|
||||
|
||||
def canal_value_to_tag(canal_value):
|
||||
return TAG_BY_CANAL.get((canal_value or "").strip().upper())
|
||||
|
||||
|
||||
def opportunity_canal_to_tag(opp_canal_value):
|
||||
"""Mapea el campo 'Canal de Origen de la Oportunidad' (Facebook|Formulario|Sucursal|...)."""
|
||||
v = (opp_canal_value or "").strip().lower()
|
||||
if not v:
|
||||
return None
|
||||
if "facebook" in v or "ig" in v or "instagram" in v:
|
||||
return "facebook-ads"
|
||||
if "formulario" in v or "form" in v or "web" in v or "whatsapp" in v:
|
||||
return "formulario"
|
||||
if "sucursal" in v or "tienda" in v or "manual" in v:
|
||||
return "sucursal"
|
||||
return None
|
||||
|
||||
|
||||
def decide(tags, fuente_value, canal_value, source, opportunity_canal_value=None):
|
||||
tags_lower = {str(t).strip().lower() for t in tags or []}
|
||||
present_targets = tags_lower & set(TARGET_TAGS)
|
||||
source_tag = classify_source(source)
|
||||
prior_canal_tag = canal_value_to_tag(canal_value)
|
||||
opp_tag = opportunity_canal_to_tag(opportunity_canal_value)
|
||||
fuente_norm = (fuente_value or "").strip().upper()
|
||||
|
||||
decided_tag = None
|
||||
confident = False
|
||||
review = False
|
||||
reason = ""
|
||||
|
||||
if len(present_targets) == 1:
|
||||
only_tag = next(iter(present_targets))
|
||||
contradicts_source = source_tag and source_tag != only_tag
|
||||
contradicts_canal = prior_canal_tag and prior_canal_tag != only_tag
|
||||
if contradicts_source and contradicts_canal:
|
||||
decided_tag = only_tag
|
||||
confident = False
|
||||
review = True
|
||||
reason = (
|
||||
f"tag único='{only_tag}', source='{source}' sugiere '{source_tag}', "
|
||||
f"canal previo='{canal_value}' -> conflicto, requiere revisión"
|
||||
)
|
||||
else:
|
||||
decided_tag = only_tag
|
||||
confident = True
|
||||
reason = f"tag único='{only_tag}'"
|
||||
if contradicts_source:
|
||||
reason += f" (source='{source}' sugiere '{source_tag}', se respeta tag)"
|
||||
elif contradicts_canal:
|
||||
reason += f" (canal previo='{canal_value}' difiere, se actualiza canal)"
|
||||
|
||||
elif len(present_targets) >= 2:
|
||||
if source_tag and source_tag in present_targets:
|
||||
decided_tag = source_tag
|
||||
confident = True
|
||||
reason = f"mix {sorted(present_targets)} -> source='{source}' apunta a '{source_tag}'"
|
||||
elif source_tag:
|
||||
decided_tag = source_tag
|
||||
confident = True
|
||||
reason = (
|
||||
f"mix {sorted(present_targets)} pero source='{source}' sugiere '{source_tag}' "
|
||||
"(fuera del mix); se respeta source"
|
||||
)
|
||||
elif prior_canal_tag and prior_canal_tag in present_targets:
|
||||
decided_tag = prior_canal_tag
|
||||
confident = True
|
||||
reason = f"mix {sorted(present_targets)} -> canal previo '{canal_value}' apunta a '{prior_canal_tag}'"
|
||||
elif prior_canal_tag:
|
||||
decided_tag = prior_canal_tag
|
||||
confident = True
|
||||
reason = (
|
||||
f"mix {sorted(present_targets)} -> canal previo '{canal_value}' "
|
||||
"(no incluido en mix actual)"
|
||||
)
|
||||
elif opp_tag and opp_tag in present_targets:
|
||||
decided_tag = opp_tag
|
||||
confident = True
|
||||
reason = (
|
||||
f"mix {sorted(present_targets)} -> oportunidad canal='{opportunity_canal_value}' "
|
||||
f"-> '{opp_tag}'"
|
||||
)
|
||||
elif opp_tag:
|
||||
decided_tag = opp_tag
|
||||
confident = True
|
||||
reason = (
|
||||
f"mix {sorted(present_targets)} -> oportunidad canal='{opportunity_canal_value}' "
|
||||
f"(ajeno al mix) -> '{opp_tag}'"
|
||||
)
|
||||
elif fuente_norm == "SUCURSAL" and "sucursal" in present_targets:
|
||||
decided_tag = "sucursal"
|
||||
confident = True
|
||||
reason = f"mix {sorted(present_targets)} -> fuente=SUCURSAL"
|
||||
elif fuente_norm == "LEAD DIGITAL":
|
||||
if "formulario" in present_targets:
|
||||
decided_tag = "formulario"
|
||||
elif "facebook-ads" in present_targets:
|
||||
decided_tag = "facebook-ads"
|
||||
if decided_tag:
|
||||
confident = True
|
||||
reason = f"mix {sorted(present_targets)} -> fuente=LEAD DIGITAL -> '{decided_tag}'"
|
||||
if not decided_tag:
|
||||
review = True
|
||||
reason = f"mix {sorted(present_targets)} sin source/canal/fuente que resuelva"
|
||||
|
||||
else:
|
||||
if source_tag:
|
||||
decided_tag = source_tag
|
||||
confident = True
|
||||
reason = f"sin tag target, source='{source}' -> '{source_tag}'"
|
||||
elif prior_canal_tag:
|
||||
decided_tag = prior_canal_tag
|
||||
confident = True
|
||||
reason = f"sin tag target, canal previo '{canal_value}' -> '{prior_canal_tag}'"
|
||||
elif opp_tag:
|
||||
decided_tag = opp_tag
|
||||
confident = True
|
||||
reason = f"sin tag target, oportunidad canal='{opportunity_canal_value}' -> '{opp_tag}'"
|
||||
elif fuente_norm == "SUCURSAL":
|
||||
decided_tag = "sucursal"
|
||||
confident = True
|
||||
reason = "sin tag target, fuente=SUCURSAL"
|
||||
elif fuente_norm == "LEAD DIGITAL":
|
||||
decided_tag = "formulario"
|
||||
confident = True
|
||||
reason = "sin tag target, fuente=LEAD DIGITAL -> default 'formulario'"
|
||||
else:
|
||||
review = True
|
||||
reason = "sin tag target, sin source claro, sin canal previo, sin fuente"
|
||||
|
||||
canal_value_out = CANAL_BY_TAG[decided_tag] if decided_tag else None
|
||||
tags_to_add = [decided_tag] if decided_tag and decided_tag not in tags_lower else []
|
||||
tags_to_remove = sorted(t for t in present_targets if decided_tag and t != decided_tag)
|
||||
|
||||
return {
|
||||
"decided_tag": decided_tag,
|
||||
"canal_value": canal_value_out,
|
||||
"fuente_value": FUENTE_BY_TAG[decided_tag] if decided_tag else None,
|
||||
"tags_to_add": tags_to_add,
|
||||
"tags_to_remove": tags_to_remove,
|
||||
"confident": confident,
|
||||
"review": review,
|
||||
"reason": reason,
|
||||
"present_targets": sorted(present_targets),
|
||||
"source_tag": source_tag,
|
||||
"prior_canal_tag": prior_canal_tag,
|
||||
}
|
||||
Reference in New Issue
Block a user