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>
95 lines
4.2 KiB
Python
Executable File
95 lines
4.2 KiB
Python
Executable File
#!/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))
|