diff --git a/airtable_client.py b/airtable_client.py index b0c164d..a9f6fff 100644 --- a/airtable_client.py +++ b/airtable_client.py @@ -8,6 +8,40 @@ class AirtableClient: self.api = Api(config.AIRTABLE_TOKEN) self.leads = self.api.table(config.AIRTABLE_BASE_ID, config.LEADS_TABLE) self.campaigns = self.api.table(config.AIRTABLE_BASE_ID, config.CAMPAIGNS_TABLE) + self.cursos = self.api.table(config.AIRTABLE_BASE_ID, "Cursos") + self.centrocurso = self.api.table(config.AIRTABLE_BASE_ID, "CENTROCURSO") + + def build_ppl_lookup(self) -> dict: + """ + Construye un dict {cursoid_text: ppl_sum} calculando el PPL de cada curso + sumando los PPL de los registros CENTROCURSO con Estado ROI = 'ABIERTO'. + + Cadena: CursoID Text → Cursos.CursoID → Cursos.CentroCurso → CENTROCURSO.PPL + Solo se usan en lotes (2 llamadas bulk), sin llamadas por campaña. + """ + # 1. Cargar CENTROCURSO ABIERTO: {record_id: ppl} + cc_records = self.centrocurso.all( + formula="{Estado ROI}='ABIERTO'", + fields=["PPL"], + ) + cc_ppl = {r["id"]: float(r["fields"].get("PPL") or 0) for r in cc_records} + + # 2. Cargar Cursos: {cursoid_number: [cc_record_ids]} + cursos_records = self.cursos.all(fields=["CursoID", "CentroCurso"]) + curso_to_cc = {} + for r in cursos_records: + cid = r["fields"].get("CursoID") + cc_ids = r["fields"].get("CentroCurso", []) + if cid is not None: + curso_to_cc[str(int(cid))] = cc_ids + + # 3. Para cada CursoID Text, sumar PPL de CENTROCURSO ABIERTO + ppl_lookup = {} + for cursoid_text, cc_ids in curso_to_cc.items(): + total_ppl = sum(cc_ppl[cc_id] for cc_id in cc_ids if cc_id in cc_ppl) + ppl_lookup[cursoid_text] = round(total_ppl, 2) + + return ppl_lookup def get_active_campaigns(self) -> list[dict]: """Lee todas las campañas activas desde 'Google Ads Campaigns'.""" @@ -27,7 +61,7 @@ class AirtableClient: }) 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) -> dict: + def sync_campaigns_from_google_ads(self, google_campaigns: list[dict], monthly_metrics: dict = None, ppl_lookup: dict = None) -> dict: """ Sincroniza campañas de Google Ads → Airtable. - Crea las campañas nuevas con el status real de Google Ads. @@ -57,11 +91,19 @@ class AirtableClient: conv = round(metrics.get("conversions", 0), 2) cost = round(metrics.get("cost", 0), 2) + # PPL calculado desde CENTROCURSO (CursoID Text viene de Airtable) + 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) + cpa_max = round(ppl * 0.70, 2) + if at_record is None: 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: + fields["PPL"] = ppl + fields["CPAMax"] = cpa_max to_create.append(fields) created.append({"name": gc["name"], "id": gid, "status": at_status}) else: @@ -84,6 +126,11 @@ class AirtableClient: changes["ConvMes"] = conv if at_record["fields"].get("CosteMes") != cost: changes["CosteMes"] = cost + if ppl_lookup is not None: + if at_record["fields"].get("PPL") != ppl: + changes["PPL"] = ppl + if at_record["fields"].get("CPAMax") != cpa_max: + changes["CPAMax"] = cpa_max if changes: to_update.append((at_record["id"], changes, gc["name"])) if change_detail: diff --git a/run.py b/run.py index 11793e7..466d8f8 100644 --- a/run.py +++ b/run.py @@ -54,7 +54,9 @@ def run(): print("→ Sincronizando campañas desde Google Ads...") google_campaigns = gads.get_all_campaigns() monthly_metrics = gads.get_monthly_metrics_all() - sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics) + print(" Calculando PPL desde CENTROCURSO...") + ppl_lookup = at.build_ppl_lookup() + sync_result = at.sync_campaigns_from_google_ads(google_campaigns, monthly_metrics, ppl_lookup) if sync_result["created"]: print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):") for c in sync_result["created"]: