#!/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))