Add GACampaignMes sync and use it as optimization source
- New GACampaignMes table tracks monthly state of campaigns with activity (ENABLED status or cost > 0 this month) - sync_gacampaignmes(): creates/updates records with PPL, CPAMax, CosteMes, ConvMes, CapTotalMes, Status for current month (numeric Mes: 1-12) - get_active_gacampaignmes(): reads GACampaignMes as source for optimization loop - get_leads_this_month_gads(): filters leads by GoogleCampaignID + utm_source (pmx/google) + utm_medium (paid_search) instead of attr_cursoid - update_gacampaignmes_leads_lake(): links leads to GACampaignMes Leads Lake field - batch_update_gacampaignmes_consejos(): saves agent advice to Consejo field - Google Ads Campaigns table simplified to catalog only (name, status, PPL) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7344cc0c52
commit
a5ce85ed32
@ -11,6 +11,7 @@ class AirtableClient:
|
|||||||
self.cursos = self.api.table(config.AIRTABLE_BASE_ID, "Cursos")
|
self.cursos = self.api.table(config.AIRTABLE_BASE_ID, "Cursos")
|
||||||
self.centrocurso = self.api.table(config.AIRTABLE_BASE_ID, "CentroCurso")
|
self.centrocurso = self.api.table(config.AIRTABLE_BASE_ID, "CentroCurso")
|
||||||
self.cursomes = self.api.table(config.AIRTABLE_BASE_ID, "CursoMes")
|
self.cursomes = self.api.table(config.AIRTABLE_BASE_ID, "CursoMes")
|
||||||
|
self.gacampaignmes = self.api.table(config.AIRTABLE_BASE_ID, "GACampaignMes")
|
||||||
|
|
||||||
MESES_ES = {
|
MESES_ES = {
|
||||||
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
1: "Enero", 2: "Febrero", 3: "Marzo", 4: "Abril",
|
||||||
@ -22,7 +23,7 @@ class AirtableClient:
|
|||||||
"""
|
"""
|
||||||
Construye en 3 llamadas bulk:
|
Construye en 3 llamadas bulk:
|
||||||
ppl_lookup: {cursoid_text: ppl_ponderado}
|
ppl_lookup: {cursoid_text: ppl_ponderado}
|
||||||
PPL = Σ (PPL_centrocurso × % invalidación_centro / 100)
|
PPL = Σ (PPL_centrocurso × % invalidación_centro)
|
||||||
solo registros CentroCurso con Estado ROI = 'ABIERTO'
|
solo registros CentroCurso con Estado ROI = 'ABIERTO'
|
||||||
cap_lookup: {cursoid_text: caping_admitido}
|
cap_lookup: {cursoid_text: caping_admitido}
|
||||||
desde CursoMes para el mes/año en curso; 0 si no existe
|
desde CursoMes para el mes/año en curso; 0 si no existe
|
||||||
@ -58,8 +59,7 @@ class AirtableClient:
|
|||||||
curso_by_recordid[r["id"]] = cursoid_text
|
curso_by_recordid[r["id"]] = cursoid_text
|
||||||
curso_to_cc[cursoid_text] = cc_ids
|
curso_to_cc[cursoid_text] = cc_ids
|
||||||
|
|
||||||
# 3. PPL ponderado por % invalidación del centro
|
# 3. PPL ponderado por % invalidación del centro (decimal: 0.85 = 85%)
|
||||||
# El campo % invalidación está en formato decimal (ej. 0.85 = 85%)
|
|
||||||
ppl_lookup = {}
|
ppl_lookup = {}
|
||||||
for cursoid_text, cc_ids in curso_to_cc.items():
|
for cursoid_text, cc_ids in curso_to_cc.items():
|
||||||
total_ppl = sum(
|
total_ppl = sum(
|
||||||
@ -84,30 +84,21 @@ class AirtableClient:
|
|||||||
|
|
||||||
return ppl_lookup, cap_lookup
|
return ppl_lookup, cap_lookup
|
||||||
|
|
||||||
def get_active_campaigns(self) -> list[dict]:
|
# ------------------------------------------------------------------ #
|
||||||
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
# Google Ads Campaigns (catálogo) #
|
||||||
records = self.campaigns.all(formula="{Status}='Activa'")
|
# ------------------------------------------------------------------ #
|
||||||
result = []
|
|
||||||
for r in records:
|
|
||||||
f = r["fields"]
|
|
||||||
result.append({
|
|
||||||
"airtable_id": r["id"],
|
|
||||||
"curso": f.get("Campaign Name", "Sin nombre"),
|
|
||||||
"cursoid_text": str(f.get("CursoID Text", "")).strip(),
|
|
||||||
"google_campaign_id": str(f.get("CampaignID", "")).strip(),
|
|
||||||
"ppl": float(f.get("PPL", 0)),
|
|
||||||
"capping_mensual": int(f.get("CapTotalMes", 0)),
|
|
||||||
"cpa_maximo": float(f.get("CPAMax", 0)),
|
|
||||||
"margen_objetivo": float(f.get("MargenObjetivo", 0)),
|
|
||||||
})
|
|
||||||
return [c for c in result if c["google_campaign_id"]] # descartar sin ID
|
|
||||||
|
|
||||||
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict], monthly_metrics: dict = None, ppl_lookup: dict = None, cap_lookup: dict = None) -> dict:
|
def sync_campaigns_from_google_ads(
|
||||||
|
self,
|
||||||
|
google_campaigns: list[dict],
|
||||||
|
monthly_metrics: dict = None,
|
||||||
|
ppl_lookup: dict = None,
|
||||||
|
cap_lookup: dict = None,
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Sincroniza campañas de Google Ads → Airtable.
|
Sincroniza el catálogo de campañas Google Ads → Airtable.
|
||||||
- Crea las campañas nuevas con el status real de Google Ads.
|
Devuelve {created, updated, at_by_gid} donde at_by_gid es el
|
||||||
- Actualiza nombre y/o status si han cambiado.
|
mapping {google_campaign_id: airtable_record} para reutilizar.
|
||||||
Devuelve un resumen con listas de campañas creadas y actualizadas.
|
|
||||||
"""
|
"""
|
||||||
STATUS_MAP = {"ENABLED": "Activa", "PAUSED": "Pausada"}
|
STATUS_MAP = {"ENABLED": "Activa", "PAUSED": "Pausada"}
|
||||||
|
|
||||||
@ -118,36 +109,24 @@ class AirtableClient:
|
|||||||
if r["fields"].get("CampaignID")
|
if r["fields"].get("CampaignID")
|
||||||
}
|
}
|
||||||
|
|
||||||
created = [] # lista de dicts {name, id, status}
|
created = []
|
||||||
updated = [] # lista de dicts {name, id, changes}
|
updated = []
|
||||||
to_create = []
|
to_create = []
|
||||||
to_update = [] # lista de (record_id, changes, name, gid, at_status)
|
to_update = []
|
||||||
|
|
||||||
for gc in google_campaigns:
|
for gc in google_campaigns:
|
||||||
gid = gc["id"]
|
gid = gc["id"]
|
||||||
at_status = STATUS_MAP.get(gc["status"], "Pausada")
|
at_status = STATUS_MAP.get(gc["status"], "Pausada")
|
||||||
at_record = at_by_gid.get(gid)
|
at_record = at_by_gid.get(gid)
|
||||||
|
|
||||||
metrics = (monthly_metrics or {}).get(gid, {})
|
# "Google Ads Campaigns" es catálogo: solo nombre, status y PPL
|
||||||
conv = round(metrics.get("conversions", 0), 2)
|
|
||||||
cost = round(metrics.get("cost", 0), 2)
|
|
||||||
|
|
||||||
# PPL y CapTotalMes calculados desde CursoID Text (viene de Airtable)
|
|
||||||
cursoid_text = str((at_record or {}).get("fields", {}).get("CursoID Text", "")).strip() if at_record else ""
|
cursoid_text = str((at_record or {}).get("fields", {}).get("CursoID Text", "")).strip() if at_record else ""
|
||||||
ppl = round((ppl_lookup or {}).get(cursoid_text, 0), 2)
|
ppl = round((ppl_lookup or {}).get(cursoid_text, 0), 2)
|
||||||
cpa_max = round(ppl * 0.70, 2)
|
|
||||||
cap = int((cap_lookup or {}).get(cursoid_text, 0))
|
|
||||||
|
|
||||||
if at_record is None:
|
if at_record is None:
|
||||||
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
|
fields = {"Campaign Name": gc["name"], "CampaignID": gid, "Status": at_status}
|
||||||
if monthly_metrics is not None:
|
|
||||||
fields["ConvMes"] = conv
|
|
||||||
fields["CosteMes"] = cost
|
|
||||||
if ppl_lookup is not None:
|
if ppl_lookup is not None:
|
||||||
fields["PPL"] = ppl
|
fields["PPL"] = ppl
|
||||||
fields["CPAMax"] = cpa_max
|
|
||||||
if cap_lookup is not None:
|
|
||||||
fields["CapTotalMes"] = cap
|
|
||||||
to_create.append(fields)
|
to_create.append(fields)
|
||||||
created.append({"name": gc["name"], "id": gid, "status": at_status})
|
created.append({"name": gc["name"], "id": gid, "status": at_status})
|
||||||
else:
|
else:
|
||||||
@ -165,43 +144,232 @@ class AirtableClient:
|
|||||||
"antes": at_record["fields"].get("Status"),
|
"antes": at_record["fields"].get("Status"),
|
||||||
"ahora": at_status,
|
"ahora": at_status,
|
||||||
}
|
}
|
||||||
if monthly_metrics is not None:
|
|
||||||
if at_record["fields"].get("ConvMes") != conv:
|
|
||||||
changes["ConvMes"] = conv
|
|
||||||
if at_record["fields"].get("CosteMes") != cost:
|
|
||||||
changes["CosteMes"] = cost
|
|
||||||
if ppl_lookup is not None:
|
if ppl_lookup is not None:
|
||||||
if at_record["fields"].get("PPL") != ppl:
|
if at_record["fields"].get("PPL") != ppl:
|
||||||
changes["PPL"] = ppl
|
changes["PPL"] = ppl
|
||||||
if at_record["fields"].get("CPAMax") != cpa_max:
|
|
||||||
changes["CPAMax"] = cpa_max
|
|
||||||
if cap_lookup is not None:
|
|
||||||
if at_record["fields"].get("CapTotalMes") != cap:
|
|
||||||
changes["CapTotalMes"] = cap
|
|
||||||
if changes:
|
if changes:
|
||||||
to_update.append((at_record["id"], changes, gc["name"]))
|
to_update.append((at_record["id"], changes, gc["name"]))
|
||||||
if change_detail:
|
if change_detail:
|
||||||
updated.append({"name": gc["name"], "id": gid, "changes": change_detail})
|
updated.append({"name": gc["name"], "id": gid, "changes": change_detail})
|
||||||
|
|
||||||
# Crear en lotes de 10
|
|
||||||
for i in range(0, len(to_create), 10):
|
for i in range(0, len(to_create), 10):
|
||||||
self.campaigns.batch_create(to_create[i:i+10])
|
self.campaigns.batch_create(to_create[i:i+10])
|
||||||
|
|
||||||
# Actualizar en lotes de 10
|
|
||||||
for i in range(0, len(to_update), 10):
|
for i in range(0, len(to_update), 10):
|
||||||
batch = [{"id": rid, "fields": changes} for rid, changes, _ in to_update[i:i+10]]
|
batch = [{"id": rid, "fields": changes} for rid, changes, _ in to_update[i:i+10]]
|
||||||
self.campaigns.batch_update(batch)
|
self.campaigns.batch_update(batch)
|
||||||
|
|
||||||
return {"created": created, "updated": updated}
|
return {"created": created, "updated": updated, "at_by_gid": at_by_gid}
|
||||||
|
|
||||||
def get_leads_this_month(self, cursoid_text: str) -> tuple[int, list[str]]:
|
# ------------------------------------------------------------------ #
|
||||||
|
# GACampaignMes (estado mensual de campañas con actividad) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def sync_gacampaignmes(
|
||||||
|
self,
|
||||||
|
google_campaigns: list[dict],
|
||||||
|
monthly_metrics: dict,
|
||||||
|
ppl_lookup: dict,
|
||||||
|
cap_lookup: dict,
|
||||||
|
at_by_gid: dict,
|
||||||
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Devuelve (count, [record_ids]) de leads del mes actual para un curso.
|
Crea/actualiza registros en GACampaignMes para el mes/año en curso.
|
||||||
Filtra por attr_cursoid (que coincide con CursoID Text de la campaña).
|
Se incluyen campañas con actividad durante el mes:
|
||||||
|
- Estado ENABLED en Google Ads, o
|
||||||
|
- Coste > 0 en monthly_metrics (activas en algún momento del mes)
|
||||||
|
"""
|
||||||
|
STATUS_MAP = {"ENABLED": "Activa", "PAUSED": "Pausada"}
|
||||||
|
now = datetime.now()
|
||||||
|
mes_num = str(now.month) # valor numérico "1"…"12"
|
||||||
|
anio_str = str(now.year)
|
||||||
|
|
||||||
|
# GIDs con actividad este mes
|
||||||
|
active_gids = {
|
||||||
|
gc["id"]
|
||||||
|
for gc in google_campaigns
|
||||||
|
if gc["status"] == "ENABLED"
|
||||||
|
or (monthly_metrics or {}).get(gc["id"], {}).get("cost", 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Registros GACampaignMes ya existentes este mes: {at_campaign_id: gcm_record}
|
||||||
|
formula = f"AND({{Mes}}='{mes_num}',{{Año}}='{anio_str}')"
|
||||||
|
existing = self.gacampaignmes.all(formula=formula)
|
||||||
|
gcm_by_at_cid = {}
|
||||||
|
for r in existing:
|
||||||
|
at_cids = r["fields"].get("CampaignID", [])
|
||||||
|
if at_cids:
|
||||||
|
gcm_by_at_cid[at_cids[0]] = r
|
||||||
|
|
||||||
|
to_create = []
|
||||||
|
to_update = []
|
||||||
|
|
||||||
|
for gc in google_campaigns:
|
||||||
|
gid = gc["id"]
|
||||||
|
if gid not in active_gids:
|
||||||
|
continue
|
||||||
|
at_record = at_by_gid.get(gid)
|
||||||
|
if not at_record:
|
||||||
|
continue
|
||||||
|
|
||||||
|
at_cid = at_record["id"]
|
||||||
|
cursoid_text = str(at_record["fields"].get("CursoID Text", "")).strip()
|
||||||
|
|
||||||
|
metrics = (monthly_metrics or {}).get(gid, {})
|
||||||
|
conv = round(metrics.get("conversions", 0), 2)
|
||||||
|
cost = round(metrics.get("cost", 0), 2)
|
||||||
|
ppl = round((ppl_lookup or {}).get(cursoid_text, 0), 2)
|
||||||
|
cpa_max = round(ppl * 0.70, 2)
|
||||||
|
cap = int((cap_lookup or {}).get(cursoid_text, 0))
|
||||||
|
at_status = STATUS_MAP.get(gc["status"], "Pausada")
|
||||||
|
|
||||||
|
gcm_record = gcm_by_at_cid.get(at_cid)
|
||||||
|
|
||||||
|
if gcm_record is None:
|
||||||
|
to_create.append({
|
||||||
|
"CampaignID": [at_cid],
|
||||||
|
"Mes": mes_num,
|
||||||
|
"Año": anio_str,
|
||||||
|
"PPL": ppl,
|
||||||
|
"CPAMax": cpa_max,
|
||||||
|
"CapTotalMes": cap,
|
||||||
|
"CosteMes": cost,
|
||||||
|
"ConvMes": conv,
|
||||||
|
"Status": at_status,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
f = gcm_record["fields"]
|
||||||
|
changes = {}
|
||||||
|
if f.get("CosteMes") != cost:
|
||||||
|
changes["CosteMes"] = cost
|
||||||
|
if f.get("ConvMes") != conv:
|
||||||
|
changes["ConvMes"] = conv
|
||||||
|
if f.get("PPL") != ppl:
|
||||||
|
changes["PPL"] = ppl
|
||||||
|
if f.get("CPAMax") != cpa_max:
|
||||||
|
changes["CPAMax"] = cpa_max
|
||||||
|
if f.get("CapTotalMes") != cap:
|
||||||
|
changes["CapTotalMes"] = cap
|
||||||
|
if f.get("Status") != at_status:
|
||||||
|
changes["Status"] = at_status
|
||||||
|
if changes:
|
||||||
|
to_update.append((gcm_record["id"], changes))
|
||||||
|
|
||||||
|
for i in range(0, len(to_create), 10):
|
||||||
|
self.gacampaignmes.batch_create(to_create[i:i+10], typecast=True)
|
||||||
|
for i in range(0, len(to_update), 10):
|
||||||
|
batch = [{"id": rid, "fields": f} for rid, f in to_update[i:i+10]]
|
||||||
|
self.gacampaignmes.batch_update(batch, typecast=True)
|
||||||
|
|
||||||
|
return {"created": len(to_create), "updated": len(to_update)}
|
||||||
|
|
||||||
|
def get_active_gacampaignmes(self) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Lee los registros GACampaignMes del mes/año en curso.
|
||||||
|
Resuelve el Google Ads campaign ID a partir del registro vinculado
|
||||||
|
en 'Google Ads Campaigns'.
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
mes_num = str(now.month)
|
||||||
|
anio_str = str(now.year)
|
||||||
|
|
||||||
|
# Mapping at_record_id → google_campaign_id
|
||||||
|
campaigns_records = self.campaigns.all(fields=["CampaignID"])
|
||||||
|
at_id_to_gid = {
|
||||||
|
r["id"]: str(r["fields"].get("CampaignID", "")).strip()
|
||||||
|
for r in campaigns_records
|
||||||
|
if r["fields"].get("CampaignID")
|
||||||
|
}
|
||||||
|
|
||||||
|
formula = f"AND({{Mes}}='{mes_num}',{{Año}}='{anio_str}')"
|
||||||
|
records = self.gacampaignmes.all(formula=formula)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for r in records:
|
||||||
|
f = r["fields"]
|
||||||
|
at_cids = f.get("CampaignID", [])
|
||||||
|
gid = at_id_to_gid.get(at_cids[0], "") if at_cids else ""
|
||||||
|
if not gid:
|
||||||
|
continue
|
||||||
|
|
||||||
|
name_list = f.get("Campaign Name (from CampaignID)", [])
|
||||||
|
name = name_list[0] if name_list else "Sin nombre"
|
||||||
|
|
||||||
|
result.append({
|
||||||
|
"airtable_id": r["id"],
|
||||||
|
"curso": name,
|
||||||
|
"cursoid_text": "",
|
||||||
|
"google_campaign_id": gid,
|
||||||
|
"ppl": float(f.get("PPL") or 0),
|
||||||
|
"capping_mensual": int(f.get("CapTotalMes") or 0),
|
||||||
|
"cpa_maximo": float(f.get("CPAMax") or 0),
|
||||||
|
"margen_objetivo": float(f.get("MargenObjetivo") or 0),
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_leads_this_month_gads(self, campaign_id: str) -> tuple[int, list[str]]:
|
||||||
|
"""
|
||||||
|
Leads del mes actual para una campaña de Google Ads.
|
||||||
|
Filtra por GoogleCampaignID (extraído de attr_referer),
|
||||||
|
attr_utm_source pmx/google y attr_utm_medium paid_search.
|
||||||
"""
|
"""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
||||||
|
formula = (
|
||||||
|
f"AND("
|
||||||
|
f"{{GoogleCampaignID}}='{campaign_id}',"
|
||||||
|
f"OR({{attr_utm_source}}='pmx',{{attr_utm_source}}='google'),"
|
||||||
|
f"{{attr_utm_medium}}='paid_search',"
|
||||||
|
f"{{creado}}>='{mes_inicio}'"
|
||||||
|
f")"
|
||||||
|
)
|
||||||
|
records = self.leads.all(formula=formula, fields=["airtable id"])
|
||||||
|
ids = [r["id"] for r in records]
|
||||||
|
return len(ids), ids
|
||||||
|
|
||||||
|
def update_gacampaignmes_leads_lake(self, gcm_record_id: str, lead_ids: list[str]) -> None:
|
||||||
|
"""Actualiza el campo 'Leads Lake' de un registro GACampaignMes."""
|
||||||
|
self.gacampaignmes.update(gcm_record_id, {"Leads Lake": lead_ids})
|
||||||
|
|
||||||
|
def batch_update_gacampaignmes_consejos(self, updates: list[tuple[str, str]]) -> None:
|
||||||
|
"""
|
||||||
|
Actualiza en lote el campo 'Consejo' de varios registros GACampaignMes.
|
||||||
|
updates: lista de (gcm_record_id, consejo_text)
|
||||||
|
"""
|
||||||
|
for i in range(0, len(updates), 10):
|
||||||
|
batch = [
|
||||||
|
{"id": rid, "fields": {"Consejo": consejo}}
|
||||||
|
for rid, consejo in updates[i:i+10]
|
||||||
|
if consejo
|
||||||
|
]
|
||||||
|
if batch:
|
||||||
|
self.gacampaignmes.batch_update(batch)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Métodos legacy (usados internamente / compatibilidad) #
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def get_active_campaigns(self) -> list[dict]:
|
||||||
|
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
||||||
|
records = self.campaigns.all(formula="{Status}='Activa'")
|
||||||
|
result = []
|
||||||
|
for r in records:
|
||||||
|
f = r["fields"]
|
||||||
|
result.append({
|
||||||
|
"airtable_id": r["id"],
|
||||||
|
"curso": f.get("Campaign Name", "Sin nombre"),
|
||||||
|
"cursoid_text": str(f.get("CursoID Text", "")).strip(),
|
||||||
|
"google_campaign_id": str(f.get("CampaignID", "")).strip(),
|
||||||
|
"ppl": float(f.get("PPL", 0)),
|
||||||
|
"capping_mensual": int(f.get("CapTotalMes", 0)),
|
||||||
|
"cpa_maximo": float(f.get("CPAMax", 0)),
|
||||||
|
"margen_objetivo": float(f.get("MargenObjetivo", 0)),
|
||||||
|
})
|
||||||
|
return [c for c in result if c["google_campaign_id"]]
|
||||||
|
|
||||||
|
def get_leads_this_month(self, cursoid_text: str) -> tuple[int, list[str]]:
|
||||||
|
"""Leads del mes actual para un curso (por attr_cursoid)."""
|
||||||
|
now = datetime.now()
|
||||||
|
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
||||||
formula = (
|
formula = (
|
||||||
f"AND("
|
f"AND("
|
||||||
f"{{attr_cursoid}}='{cursoid_text}',"
|
f"{{attr_cursoid}}='{cursoid_text}',"
|
||||||
@ -213,5 +381,5 @@ class AirtableClient:
|
|||||||
return len(ids), ids
|
return len(ids), ids
|
||||||
|
|
||||||
def update_campaign_leads_lake(self, campaign_airtable_id: str, lead_ids: list[str]) -> None:
|
def update_campaign_leads_lake(self, campaign_airtable_id: str, lead_ids: list[str]) -> None:
|
||||||
"""Actualiza el campo 'Leads Lake' de una campaña con los IDs de leads del mes."""
|
"""Actualiza el campo 'Leads Lake' de una campaña en Google Ads Campaigns."""
|
||||||
self.campaigns.update(campaign_airtable_id, {"Leads Lake": lead_ids})
|
self.campaigns.update(campaign_airtable_id, {"Leads Lake": lead_ids})
|
||||||
|
|||||||
26
run.py
26
run.py
@ -104,10 +104,18 @@ def run():
|
|||||||
print(f" ~ [{c['id']}] {c['name']} | {field}: '{val['antes']}' → '{val['ahora']}'")
|
print(f" ~ [{c['id']}] {c['name']} | {field}: '{val['antes']}' → '{val['ahora']}'")
|
||||||
if not sync_result["created"] and not sync_result["updated"]:
|
if not sync_result["created"] and not sync_result["updated"]:
|
||||||
print(" ✓ Sin cambios en el catálogo.")
|
print(" ✓ Sin cambios en el catálogo.")
|
||||||
|
|
||||||
|
# Sincronizar GACampaignMes (campañas con actividad este mes)
|
||||||
|
print(" Sincronizando GACampaignMes...")
|
||||||
|
gcm_result = at.sync_gacampaignmes(
|
||||||
|
google_campaigns, monthly_metrics, ppl_lookup, cap_lookup,
|
||||||
|
sync_result["at_by_gid"]
|
||||||
|
)
|
||||||
|
print(f" ✓ GACampaignMes: {gcm_result['created']} nuevas, {gcm_result['updated']} actualizadas.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
campaigns = at.get_active_campaigns()
|
campaigns = at.get_active_gacampaignmes()
|
||||||
print(f"→ {len(campaigns)} campañas activas encontradas")
|
print(f"→ {len(campaigns)} campañas con actividad este mes")
|
||||||
print("→ Analizando...\n")
|
print("→ Analizando...\n")
|
||||||
|
|
||||||
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
|
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
|
||||||
@ -117,8 +125,8 @@ def run():
|
|||||||
for campaign in campaigns:
|
for campaign in campaigns:
|
||||||
cid = campaign["google_campaign_id"]
|
cid = campaign["google_campaign_id"]
|
||||||
|
|
||||||
leads, lead_ids = at.get_leads_this_month(campaign["cursoid_text"])
|
leads, lead_ids = at.get_leads_this_month_gads(cid)
|
||||||
at.update_campaign_leads_lake(campaign["airtable_id"], lead_ids)
|
at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids)
|
||||||
|
|
||||||
metrics = gads.get_campaign_metrics(cid)
|
metrics = gads.get_campaign_metrics(cid)
|
||||||
if not metrics:
|
if not metrics:
|
||||||
@ -147,6 +155,7 @@ def run():
|
|||||||
|
|
||||||
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
||||||
resumen = []
|
resumen = []
|
||||||
|
consejo_updates = [] # (gcm_record_id, consejo) para batch update final
|
||||||
last_priority = -1
|
last_priority = -1
|
||||||
|
|
||||||
for item in collected:
|
for item in collected:
|
||||||
@ -196,6 +205,9 @@ def run():
|
|||||||
|
|
||||||
apply_decision(campaign, decision, metrics, gads)
|
apply_decision(campaign, decision, metrics, gads)
|
||||||
|
|
||||||
|
if decision.get("consejo"):
|
||||||
|
consejo_updates.append((campaign["airtable_id"], decision["consejo"]))
|
||||||
|
|
||||||
resumen.append({
|
resumen.append({
|
||||||
"curso": campaign["curso"],
|
"curso": campaign["curso"],
|
||||||
"urgencia": analysis["urgencia"],
|
"urgencia": analysis["urgencia"],
|
||||||
@ -210,6 +222,12 @@ def run():
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
# Guardar consejos en GACampaignMes
|
||||||
|
if consejo_updates:
|
||||||
|
print(f"→ Guardando {len(consejo_updates)} consejos en GACampaignMes...")
|
||||||
|
at.batch_update_gacampaignmes_consejos(consejo_updates)
|
||||||
|
print(" ✓ Consejos guardados.")
|
||||||
|
|
||||||
# Resumen final ordenado por prioridad (ya está ordenado)
|
# Resumen final ordenado por prioridad (ya está ordenado)
|
||||||
print(f"{'='*55}")
|
print(f"{'='*55}")
|
||||||
print("RESUMEN FINAL")
|
print("RESUMEN FINAL")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user