fix: mostrar Google Ads desde ad_daily cuando kpi_weekly está vacío

kpi_weekly es una MV de solo lectura sin canales paid; canal/seguimiento
ahora hacen fallback a ad_daily. Bump Google Ads API a v21 en sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-23 12:13:41 +02:00
parent a1a00840d5
commit fba025da76
7 changed files with 159 additions and 24 deletions

94
scripts/ingest_gads_json.py Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Ingest .ops/gads/{customer_id}.json MCP dumps into ad_daily."""
from __future__ import annotations
import json, os, sys, urllib.request, urllib.error
from pathlib import Path
from datetime import datetime, timezone, timedelta
ROOT = Path('/Users/4ventures/4ventures-client-portal')
# load .env.local
for line in (ROOT/'.env.local').read_text().splitlines():
if '=' in line and not line.startswith('#') and line.split('=',1)[0] not in os.environ:
k,v=line.split('=',1); os.environ[k]=v.strip().strip('"').strip("'")
url=os.environ['NEXT_PUBLIC_SUPABASE_URL'].rstrip('/')
key=os.environ['SUPABASE_SERVICE_ROLE_KEY']
h={'apikey':key,'Authorization':f'Bearer {key}','Content-Type':'application/json'}
MAP={
'3019377678':'a7a6eb49-435c-411e-bdb0-a45b6a75ffab',
'7658651782':'2f6587f1-5dab-4994-b99f-d62f4300dd92',
'4975185233':'25d3cdda-a3fc-4af7-b733-b38304b489df',
'3183140753':'506a6d00-dc5a-4d84-bedc-73a35e3014a6',
'8746480744':'98d023ba-1c5c-4cab-8ab9-4e70c4367bb1',
'7181044127':'6f5c19e4-bf8b-4ef0-8c7e-55c9d0c0a041',
'8436023380':'97def34e-e1c4-41b4-a579-036b14cd5091',
'2986275882':'229687b5-6861-4b6d-b321-e09197629731',
'6481978772':'8e5761b7-6347-4fd1-acbf-4572730a783a',
'9069365731':'73a1b415-2325-4cab-9769-823021a5e888',
'8177742252':'730feaf2-f2a3-4a18-963b-12b853551d8d',
'9627541456':'fd7d361b-c4d0-47ca-bc0e-75d21ac50da3',
'8592129580':'7df5293e-b954-4059-94da-d18534a08767',
'6871069003':'c538a248-4ef0-437b-b4a0-544cff597659',
'2508925045':'a5a250b4-e79a-4cb3-bfa1-4f9a6d908a03',
'4418211066':'48919ad2-6af0-4ef0-96a3-fd9ca0a46cd2',
'1323363601':'0b3c5fc0-6901-4caf-b26a-b6a0201d8541',
}
def call(method, path, body=None):
headers=dict(h); headers['Prefer']='return=minimal'
data=None if body is None else json.dumps(body).encode()
req=urllib.request.Request(f'{url}{path}', data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as r:
return r.status, r.read().decode()
except urllib.error.HTTPError as e:
return e.code, e.read().decode()[:400]
since=(datetime.now(timezone.utc)-timedelta(days=30)).date().isoformat()
gads=ROOT/'.ops'/'gads'
total_rows=0; total_spend=0.0
for path in sorted(gads.glob('*.json')):
cid=path.stem
tid=MAP.get(cid)
if not tid:
print('skip unknown', cid); continue
rows_raw=json.loads(path.read_text())
if not rows_raw:
print(cid, 'EMPTY'); continue
out=[]
for r in rows_raw:
date=r.get('segments.date') or (r.get('segments') or {}).get('date')
if not date: continue
camp_id=r.get('campaign.id') or (r.get('campaign') or {}).get('id')
camp_name=r.get('campaign.name') or (r.get('campaign') or {}).get('name')
imp=r.get('metrics.impressions') or (r.get('metrics') or {}).get('impressions') or 0
clicks=r.get('metrics.clicks') or (r.get('metrics') or {}).get('clicks') or 0
cost=r.get('metrics.cost_micros') or (r.get('metrics') or {}).get('costMicros') or (r.get('metrics') or {}).get('cost_micros') or 0
conv=r.get('metrics.conversions') or (r.get('metrics') or {}).get('conversions') or 0
cid_s = str(camp_id) if camp_id is not None else ''
out.append({
'tenant_id': tid,
'date': date,
'platform': 'google',
'campaign_id': cid_s or None,
'campaign_name': camp_name,
# ad_daily.ad_id is NOT NULL — campaign-level rows reuse campaign id
'ad_id': cid_s,
'ad_name': '',
'impressions': int(float(imp or 0)),
'clicks': int(float(clicks or 0)),
'spend': round(float(cost or 0)/1_000_000, 2),
'platform_leads': int(round(float(conv or 0))),
})
call('DELETE', f'/rest/v1/ad_daily?tenant_id=eq.{tid}&platform=eq.google&date=gte.{since}')
ok=True
for i in range(0,len(out),400):
st,res=call('POST','/rest/v1/ad_daily', out[i:i+400])
if st not in (200,201):
print('FAIL', cid, st, res); ok=False; break
if ok:
spend=sum(x['spend'] for x in out)
total_rows+=len(out); total_spend+=spend
print(f'OK {cid} rows={len(out)} spend={spend:.2f}')
print('TOTAL', total_rows, round(total_spend,2))