Merge pull request #1 from jmgomezroi/claude/help-project-setup-t4hyw
Claude/help project setup t4hyw
This commit is contained in:
commit
ef7d494aeb
@ -11,32 +11,77 @@ class AirtableClient:
|
||||
|
||||
def get_active_campaigns(self) -> list[dict]:
|
||||
"""Lee todas las campañas activas desde 'Google Ads Campaigns'."""
|
||||
records = self.campaigns.all(formula="{Activa}=TRUE()")
|
||||
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("CPAMaximo", 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 get_leads_this_month(self, google_campaign_id: str) -> int:
|
||||
def sync_campaigns_from_google_ads(self, google_campaigns: list[dict]) -> dict:
|
||||
"""
|
||||
Cuenta todos los leads del mes actual para una campaña.
|
||||
Todos los leads en Airtable están validados, sin filtro de estado.
|
||||
Sincroniza campañas de Google Ads → Airtable.
|
||||
- Crea las campañas nuevas con el status real de Google Ads.
|
||||
- Actualiza nombre y/o status si han cambiado.
|
||||
Devuelve un resumen con listas de campañas creadas y actualizadas.
|
||||
"""
|
||||
STATUS_MAP = {"ENABLED": "Activa", "PAUSED": "Pausada"}
|
||||
|
||||
all_records = self.campaigns.all()
|
||||
at_by_gid = {
|
||||
str(r["fields"].get("CampaignID", "")).strip(): r
|
||||
for r in all_records
|
||||
if r["fields"].get("CampaignID")
|
||||
}
|
||||
|
||||
created = []
|
||||
updated = []
|
||||
|
||||
for gc in google_campaigns:
|
||||
gid = gc["id"]
|
||||
at_status = STATUS_MAP.get(gc["status"], "Pausada")
|
||||
at_record = at_by_gid.get(gid)
|
||||
|
||||
if at_record is None:
|
||||
self.campaigns.create({
|
||||
"Campaign Name": gc["name"],
|
||||
"CampaignID": int(gid),
|
||||
"Status": at_status,
|
||||
})
|
||||
created.append(gc["name"])
|
||||
else:
|
||||
changes = {}
|
||||
if at_record["fields"].get("Campaign Name") != gc["name"]:
|
||||
changes["Campaign Name"] = gc["name"]
|
||||
if at_record["fields"].get("Status") != at_status:
|
||||
changes["Status"] = at_status
|
||||
if changes:
|
||||
self.campaigns.update(at_record["id"], changes)
|
||||
updated.append(gc["name"])
|
||||
|
||||
return {"created": created, "updated": updated}
|
||||
|
||||
def get_leads_this_month(self, cursoid_text: str) -> int:
|
||||
"""
|
||||
Cuenta todos los leads validados del mes actual para un curso.
|
||||
Filtra por attr_cursoid (que coincide con CursoID Text de la campaña).
|
||||
"""
|
||||
now = datetime.now()
|
||||
mes_inicio = f"{now.year}-{now.month:02d}-01"
|
||||
|
||||
formula = (
|
||||
f"AND("
|
||||
f"{{GoogleCampaignID}}='{google_campaign_id}',"
|
||||
f"{{Fecha}}>='{mes_inicio}'"
|
||||
f"{{attr_cursoid}}='{cursoid_text}',"
|
||||
f"{{creado}}>='{mes_inicio}'"
|
||||
f")"
|
||||
)
|
||||
records = self.leads.all(formula=formula)
|
||||
|
||||
@ -11,6 +11,7 @@ def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) ->
|
||||
capping = campaign_config["capping_mensual"]
|
||||
ppl = campaign_config["ppl"]
|
||||
cpa_max = campaign_config["cpa_maximo"]
|
||||
margen_objetivo = campaign_config.get("margen_objetivo", 0)
|
||||
gasto = ads_metrics.get("cost", 0)
|
||||
conversiones_google = ads_metrics.get("conversions", 0)
|
||||
|
||||
@ -42,6 +43,8 @@ def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) ->
|
||||
"campaign_id": campaign_config["google_campaign_id"],
|
||||
"ppl": ppl,
|
||||
"cpa_maximo": cpa_max,
|
||||
"margen_objetivo": margen_objetivo,
|
||||
"margen_ok": margen >= margen_objetivo if margen_objetivo > 0 else True,
|
||||
"capping": capping,
|
||||
"leads_entregados": leads_entregados,
|
||||
"leads_restantes": leads_restantes,
|
||||
|
||||
@ -21,4 +21,3 @@ ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
|
||||
|
||||
# Operación
|
||||
DRY_RUN = True # True = solo sugiere, no aplica cambios en Google Ads
|
||||
MARGEN_MINIMO = 0.40 # 40% mínimo de margen sobre PPL
|
||||
|
||||
@ -15,6 +15,32 @@ class GoogleAdsClient:
|
||||
})
|
||||
self.customer_id = config.GOOGLE_ADS_LOGIN_CUSTOMER_ID
|
||||
|
||||
def get_all_campaigns(self) -> list[dict]:
|
||||
"""Obtiene todas las campañas no eliminadas de la cuenta."""
|
||||
ga_service = self.client.get_service("GoogleAdsService")
|
||||
query = """
|
||||
SELECT
|
||||
campaign.id,
|
||||
campaign.name,
|
||||
campaign.status
|
||||
FROM campaign
|
||||
WHERE campaign.status != 'REMOVED'
|
||||
ORDER BY campaign.name
|
||||
"""
|
||||
try:
|
||||
response = ga_service.search(customer_id=self.customer_id, query=query)
|
||||
return [
|
||||
{
|
||||
"id": str(row.campaign.id),
|
||||
"name": row.campaign.name,
|
||||
"status": row.campaign.status.name,
|
||||
}
|
||||
for row in response
|
||||
]
|
||||
except GoogleAdsException as e:
|
||||
print(f" ❌ Error obteniendo campañas de Google Ads: {e}")
|
||||
return []
|
||||
|
||||
def get_campaign_metrics(self, campaign_id: str) -> dict:
|
||||
"""Métricas del mes en curso para una campaña concreta."""
|
||||
ga_service = self.client.get_service("GoogleAdsService")
|
||||
|
||||
18
run.py
18
run.py
@ -25,6 +25,22 @@ def run():
|
||||
at = AirtableClient()
|
||||
gads = GoogleAdsClient()
|
||||
|
||||
# Sincronizar catálogo de campañas desde Google Ads → Airtable
|
||||
print("→ Sincronizando campañas desde Google Ads...")
|
||||
google_campaigns = gads.get_all_campaigns()
|
||||
sync_result = at.sync_campaigns_from_google_ads(google_campaigns)
|
||||
if sync_result["created"]:
|
||||
print(f" ✅ Campañas nuevas importadas ({len(sync_result['created'])}):")
|
||||
for name in sync_result["created"]:
|
||||
print(f" + {name}")
|
||||
if sync_result["updated"]:
|
||||
print(f" 🔄 Campañas con nombre actualizado ({len(sync_result['updated'])}):")
|
||||
for name in sync_result["updated"]:
|
||||
print(f" ~ {name}")
|
||||
if not sync_result["created"] and not sync_result["updated"]:
|
||||
print(" ✓ Sin cambios en el catálogo.")
|
||||
print()
|
||||
|
||||
campaigns = at.get_active_campaigns()
|
||||
print(f"→ {len(campaigns)} campañas activas encontradas\n")
|
||||
|
||||
@ -37,7 +53,7 @@ def run():
|
||||
print(f" Campaign ID: {cid} | PPL: {campaign['ppl']}€ | Cap: {campaign['capping_mensual']} leads")
|
||||
|
||||
# 1. Leads reales desde Airtable
|
||||
leads = at.get_leads_this_month(cid)
|
||||
leads = at.get_leads_this_month(campaign["cursoid_text"])
|
||||
|
||||
# 2. Métricas de Google Ads
|
||||
metrics = gads.get_campaign_metrics(cid)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user