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

View File

@@ -11,6 +11,7 @@ import {
AdDailyRow,
KpiWeeklyRow,
buildChannelSeries,
resolveWeeklyRows,
sumDaily,
sumWeekly,
} from '@/lib/metrics';
@@ -76,7 +77,7 @@ export async function GET() {
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
),
restGet<AdDailyRow[]>(
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.asc&limit=800`
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=5000`
),
]);
entries = e.data || [];
@@ -93,14 +94,15 @@ export async function GET() {
supabase
.from('ad_daily')
.select('*')
.order('date', { ascending: true })
.limit(800),
.order('date', { ascending: false })
.limit(5000),
]);
entries = (e as SeguimientoEntry[]) || [];
weekly = (w as KpiWeeklyRow[]) || [];
daily = (d as AdDailyRow[]) || [];
}
weekly = resolveWeeklyRows(weekly, daily);
const channelSeries = buildChannelSeries(weekly);
const totalsWeekly = sumWeekly(weekly);
const totalsDaily = sumDaily(daily);

View File

@@ -5,10 +5,12 @@ import { redirect } from 'next/navigation';
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
import { isAgencyRole, userRole } from '@/lib/agency';
import {
AdDailyRow,
KpiWeeklyRow,
buildChannelSeries,
formatEur,
formatNum,
resolveWeeklyRows,
} from '@/lib/metrics';
import { ArrowLeft } from 'lucide-react';
@@ -40,20 +42,33 @@ export default async function CanalDashPage({
}
let weekly: KpiWeeklyRow[] = [];
let daily: AdDailyRow[] = [];
if (agency && ctx.viaView && ctx.tenantId) {
const tid = encodeURIComponent(ctx.tenantId);
const w = await restGet<KpiWeeklyRow[]>(
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
);
const [w, d] = await Promise.all([
restGet<KpiWeeklyRow[]>(
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.asc`
),
restGet<AdDailyRow[]>(
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=5000`
),
]);
weekly = w.data || [];
daily = d.data || [];
} else {
const { data } = await supabase
.from('kpi_weekly')
.select('*')
.order('week', { ascending: true });
weekly = (data as KpiWeeklyRow[]) || [];
const [{ data: w }, { data: d }] = await Promise.all([
supabase.from('kpi_weekly').select('*').order('week', { ascending: true }),
supabase
.from('ad_daily')
.select('*')
.order('date', { ascending: false })
.limit(5000),
]);
weekly = (w as KpiWeeklyRow[]) || [];
daily = (d as AdDailyRow[]) || [];
}
weekly = resolveWeeklyRows(weekly, daily);
const series = buildChannelSeries(weekly);
const points = series.points.filter((p) => p.channel === channel);
const spend = points.reduce((a, p) => a + p.spend, 0);

View File

@@ -8,9 +8,9 @@ import {
KpiWeeklyRow,
buildChannelSeries,
buildComparisons,
resolveWeeklyRows,
sumDaily,
sumWeekly,
weeklyFromDaily,
} from '@/lib/metrics';
import { isAgencyRole, userRole } from '@/lib/agency';
@@ -65,13 +65,7 @@ export default async function KpisPage() {
daily = (d as AdDailyRow[]) || [];
}
// kpi_weekly is often a stale MV — prefer ad_daily when it has more signal
const fromDaily = weeklyFromDaily(daily);
const weeklySpend = weekly.reduce((a, r) => a + Number(r.total_spend || 0), 0);
const dailySpend = daily.reduce((a, r) => a + Number(r.spend || 0), 0);
if (fromDaily.length && dailySpend >= weeklySpend) {
weekly = fromDaily;
}
weekly = resolveWeeklyRows(weekly, daily);
const comparisons = buildComparisons(weekly);
const month =

View File

@@ -349,7 +349,8 @@ export default function MetricsDashboard({
{channelRows.length === 0 ? (
<p className="text-xs text-gray-500 py-6 text-center">
Sin series de canal en el periodo. Cuando Windsor cargue
`kpi_weekly.channel`, aparecerán aquí.
Los canales con inversión en ads (Meta, Google) aparecen cuando hay
filas en ad_daily o kpi_weekly para este cliente.
</p>
) : (
<div className="overflow-x-auto">

View File

@@ -4,7 +4,7 @@ import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
const META_GRAPH = 'https://graph.facebook.com/v21.0';
const GADS_API = 'https://googleads.googleapis.com/v18';
const GADS_API = 'https://googleads.googleapis.com/v21';
export type SyncTenantResult = {
tenantId: string;
@@ -285,14 +285,16 @@ async function fetchGoogleCampaignDaily(
const date = row.segments?.date;
if (!date) continue;
const spend = Math.round((n(row.metrics?.costMicros) / 1_000_000) * 100) / 100;
const campaignId = row.campaign?.id ? String(row.campaign.id) : null;
out.push({
tenant_id: tenantId,
date,
platform: 'google',
campaign_id: row.campaign?.id ? String(row.campaign.id) : null,
campaign_id: campaignId,
campaign_name: row.campaign?.name || null,
ad_id: null,
ad_name: null,
// ad_daily.ad_id is NOT NULL — campaign-level rows reuse campaign id
ad_id: campaignId || '',
ad_name: '',
impressions: n(row.metrics?.impressions),
clicks: n(row.metrics?.clicks),
spend,

View File

@@ -284,4 +284,31 @@ export function weeklyFromDaily(rows: AdDailyRow[]): KpiWeeklyRow[] {
});
}
const ADS_CHANNELS = new Set(['meta', 'google']);
/**
* kpi_weekly is often a non-writable MV without paid channels.
* Prefer ad_daily rollups when they carry meta/google (or more spend).
*/
export function resolveWeeklyRows(
weekly: KpiWeeklyRow[],
daily: AdDailyRow[]
): KpiWeeklyRow[] {
const fromDaily = weeklyFromDaily(daily);
if (!fromDaily.length) return weekly;
const weeklySpend = weekly.reduce((a, r) => a + n(r.total_spend), 0);
const dailySpend = daily.reduce((a, r) => a + n(r.spend), 0);
const weeklyAds = weekly.some((r) =>
ADS_CHANNELS.has((r.channel || '').toLowerCase())
);
const dailyAds = daily.some((r) =>
ADS_CHANNELS.has((r.platform || '').toLowerCase())
);
if (dailyAds && !weeklyAds) return fromDaily;
if (dailySpend >= weeklySpend) return fromDaily;
return weekly;
}
export { isoDate, weekStart };