/**
* API Route: /api/ads
*
* GET: Fetch targeted advertisements for the current user
* POST: Track ad events (impressions/clicks)
*
* Supports geo-targeting by postal code and province.
* PRO users do not receive ads.
*/
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/auth';
import { supabase } from '@/lib/supabase-admin';
import { getProvinceFromPostalCode, AD_CONFIG, type TargetedAd, type GetAdsResponse } from '@/types/ads';
/**
* GET /api/ads - Fetch targeted ads
*
* Query params:
* - postal_code: User's postal code for geo-targeting
* - locale: Language (en/fr)
* - placement: Ad placement type (feed_inline, feed_top, carousel)
* - limit: Max number of ads to return
*/
export async function GET(request: NextRequest) {
try {
const session = await auth();
// Check if user is PRO (ad-free)
if (session?.user?.id) {
const { data: profile } = await supabase
.from('user_profiles')
.select('subscription_tier')
.eq('id', session.user.id)
.single();
if (profile?.subscription_tier && AD_CONFIG.hideForTiers.includes(profile.subscription_tier)) {
return NextResponse.json({ ads: [] } satisfies GetAdsResponse);
}
}
// Parse query parameters
const searchParams = request.nextUrl.searchParams;
const postalCode = searchParams.get('postal_code') || null;
const locale = searchParams.get('locale') || 'en';
const placement = searchParams.get('placement') || 'feed_inline';
const limit = Math.min(parseInt(searchParams.get('limit') || '5'), 10);
// Derive province from postal code
const province = getProvinceFromPostalCode(postalCode);
// If user is logged in, try to get postal code from profile
let effectivePostalCode = postalCode;
let effectiveProvince = province;
if (!postalCode && session?.user?.id) {
const { data: profile } = await supabase
.from('user_profiles')
.select('postal_code')
.eq('id', session.user.id)
.single();
if (profile?.postal_code) {
effectivePostalCode = profile.postal_code;
effectiveProvince = getProvinceFromPostalCode(profile.postal_code);
}
}
// Fetch targeted ads using the database function
const { data: ads, error } = await supabase.rpc('get_targeted_ads', {
p_postal_code: effectivePostalCode,
p_province: effectiveProvince,
p_locale: locale,
p_placement: placement,
p_limit: limit,
});
if (error) {
console.error('Error fetching targeted ads:', error);
// Return empty array instead of error to avoid breaking the feed
return NextResponse.json({ ads: [] } satisfies GetAdsResponse);
}
const response: GetAdsResponse = {
ads: (ads || []) as TargetedAd[],
};
return NextResponse.json(response);
} catch (error) {
console.error('Error in GET /api/ads:', error);
// Return empty array to avoid breaking the feed
return NextResponse.json({ ads: [] } satisfies GetAdsResponse);
}
}
/**
* POST /api/ads - Track ad event (impression or click)
*
* Body:
* - ad_id: UUID of the advertisement
* - event_type: 'impression' or 'click'
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { ad_id, event_type } = body;
if (!ad_id || !event_type) {
return NextResponse.json(
{ error: 'Missing required fields: ad_id, event_type' },
{ status: 400 }
);
}
if (!['impression', 'click'].includes(event_type)) {
return NextResponse.json(
{ error: 'Invalid event_type. Must be "impression" or "click"' },
{ status: 400 }
);
}
// Track the event
const { error } = await supabase.rpc('track_ad_event', {
p_ad_id: ad_id,
p_event_type: event_type,
});
if (error) {
console.error('Error tracking ad event:', error);
// Don't fail the request, just log the error
return NextResponse.json({ success: false });
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in POST /api/ads:', error);
return NextResponse.json({ success: false });
}
}