Leads submitted via Google Lead Form arrive with UserAgent
'Google-Ads-Notifications' and campaign name in attr_referer.
Added this as a fourth attribution clause in get_leads_this_month_gads()
to reduce discrepancy between Airtable and Google Ads conversion counts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use CosteMes and ConvMes from all previous month fco_ campaigns (not just
those active in current month) to avoid missing paused campaigns
- Show day-1 cierre block with Google Ads data labeled explicitly
- Show current month as 0 on day 1 (no data yet)
- Fetch Campaign Name in get_metricas_diarias_prev_month for fco_ filtering
- Fetch prev month data for days 1-5 (last 5 days can span two months)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New slack_reporter.py: posts daily summary to Slack after each run
- Shows monthly investment, revenue (sumatorio + leadsxPPL) and margins
- Rankings of best/worst fco_ campaigns for last 5 days and current month
- Alerts for campaigns with losses > 200€ or margin < -50%
- MetricasDiarias uses yesterday's data (Google Ads lag)
- SLACK_WEBHOOK_URL added to config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each optimizer run writes yesterday's cost, revenue (conversions x PPL)
and margin per campaign as a JSON entry keyed by day number into the
MetricasDiarias field. Yesterday is used because Google Ads data for
the current day is not yet available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detect courses with _leadform companion campaigns before first pass
- Add ℹ️ LEADFORM note in Log for campaigns that use Google's native lead form (leads bypass the website and don't reach Airtable)
- Add ⚠️ LEADFORM COMPANION note for sibling campaigns of the same course (explains conversion count discrepancy)
- Also moves _course_num and courses_with_both detection to pre-pass (before first loop) so PMX analysis uses correct leads_grupo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Detect Search+PMX companion campaigns by course number
- Add PMX attribution warning to Log field for Search campaigns with active PMX
- Add ConvLeadsLakeMesGrupo: for PMX with Search companion uses Search leads count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix GoogleCampaignID matching with comma delimiters (exact word boundary)
- Remove attr_utm_medium filter (campaign ID filter is specific enough)
- Fix get_campaign_metrics: accumulate all daily rows for monthly totals
- Fix SPRINT urgency: only trigger when capping > 0
- Add Log field to GACampaignMes with Airtable vs Google Ads discrepancy info
- batch_update_gacampaignmes_advice now also updates Log field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use GACampaignID field + FIND(gad_campaignid) fallback in Leads Lake filter
- Add ConvLeadsLakeMes to campaign data for tracking discrepancy comparison
- Update ConvLeadsLakeMesFinal daily as snapshot of Airtable lead count
- Agent: ignore capping when capping=0 in advice
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- batch_update_gacampaignmes_advice() replaces consejos method, also saves Criticidad
- Criticidad mapped from priority: Crítico (PAUSAR/SPRINT), Peligro, Mantener
- Updated in same batch as Consejo after second pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_active_gacampaignmes() now returns campaign_at_id (Google Ads Campaigns record ID)
- New batch_update_status() updates Status in GACampaignMes and Google Ads Campaigns
- run.py first pass collects status from get_campaign_metrics() and batch-updates
both tables at end of pass, ensuring fresh status after each run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- analyzer.py: add status_google (ENABLED/PAUSED) to analysis dict
- agent.py: instruct agent never to recommend reactivating an ENABLED campaign;
mention reactivation in consejo only when campaign is PAUSED
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
- Campaigns now shown in three sections:
1. ACCIONES CRÍTICAS (urgencia PAUSAR/SPRINT)
2. ACCIONES NO CRÍTICAS (other budget changes)
3. SIN CAMBIOS (MANTENER)
- First pass collects all data, then sorts before printing
- Each campaign shows direct Google Ads link with account ID
- Summary table also grouped by section with links
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PPL = sum(PPL_centrocurso × % invalidación_centro) for ABIERTO records
(% invalidación stored as decimal 0-1, not percentage)
- CapTotalMes synced from CursoMes.Caping Admitido for current month/year
(0 if no record found in CursoMes)
- Table reference fixed: CENTROCURSO → CentroCurso
- Added CursoMes table client in AirtableClient.__init__
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_leads_this_month() now returns (count, [record_ids])
- update_campaign_leads_lake() updates the multipleRecordLinks
field 'Leads Lake' in Google Ads Campaigns with the lead IDs
found for that course this month
- run.py calls the update after fetching leads per campaign
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add build_ppl_lookup(): loads Cursos + CENTROCURSO ABIERTO in 2
bulk calls, returns {cursoid_text: sum_ppl} for all courses
- sync_campaigns_from_google_ads() now accepts ppl_lookup and
updates PPL = sum(CENTROCURSO.PPL where Estado ROI=ABIERTO)
and CPAMax = PPL * 70% for each campaign
- run.py builds the lookup before sync and passes it in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add get_monthly_metrics_all() in GoogleAdsClient: fetches
conversions and cost for all campaigns in one query
- sync_campaigns_from_google_ads() now accepts monthly_metrics
and updates ConvMes and CosteMes fields in Airtable
- run.py fetches monthly metrics before sync and passes them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sync shows campaign ID, name and status for new campaigns,
and field-level changes (before/after) for updated ones
- Agent now returns a 'consejo' field with a specific actionable
recommendation per campaign
- Summary table includes leads, margin and consejo per campaign
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each run saves a timestamped log file (logs/YYYYMMDD_HHMMSS.log)
with full output. logs/ is gitignored.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Force UTF-8 stdout with line buffering to fix Windows encoding error
- Use batch_create/batch_update in Airtable sync (10x faster)
- Fix CampaignID field type (string, not int)
- Fix FechaEntrada → Fecha in leads query
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Campaigns: Status='Activa' filter (was Activa=TRUE), CPAMax (was
CPAMaximo), add MargenObjetivo and CursoID Text per campaign
- Leads: filter by attr_cursoid + creado (was GoogleCampaignID + Fecha)
- analyzer: expose margen_objetivo and margen_ok per campaign
- config: remove global MARGEN_MINIMO, now per-campaign via Airtable
- sync: new campaigns created with Status='Pausada'
https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2
Before each run, fetch all non-removed campaigns from Google Ads
and upsert them into the Airtable campaigns table: new campaigns
are created (Activa=False, pending manual PPL/Cap/CPA setup) and
existing ones get their name updated if it changed.
https://claude.ai/code/session_01WEcdWPxGWZKk8FGwBRLGR2