← back to Norma
app/api/leads/discover/route.ts
132 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getBrand } from '@/lib/brand';
const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
/**
* POST /api/leads/discover
* AI discovers old leads that went cold and are worth rekindling.
* Looks at collaborations that were suggested but never followed up,
* and generates realistic cold-lead scenarios for the organization.
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
// Get existing leads to avoid duplicates
const existing = await query<{ name: string }>('SELECT name FROM old_leads');
const existingNames = existing.rows.map(r => r.name);
// Get collaborations that could be cold leads
const collabs = await query<{ name: string; organization: string; collab_type: string; ai_reason: string }>(
"SELECT name, organization, collab_type, ai_reason FROM collaborations WHERE status = 'suggested' LIMIT 10"
);
// Get recent news context
const newsRes = await query<{ headline: string }>(
'SELECT headline FROM news_items ORDER BY published_at DESC NULLS LAST LIMIT 10'
);
const brand = await getBrand();
const prompt = `You are a lead recovery specialist for ${brand.name} (${brand.shortName}), a non-profit focused on ${brand.focusAreas.join(', ')}.
${brand.shortName} has been operating for years and has had many warm contacts that went cold. Generate 8-10 realistic old leads — organizations, individuals, government contacts, and media outlets that ${brand.shortName} likely reached out to in the past but lost touch with.
For each lead, provide:
- name: Organization or person name
- contact_name: Key contact person (if org)
- contact_title: Their title
- organization: Organization name
- lead_type: One of: nonprofit, government, corporation, university, media, foundation, individual, other
- email: Realistic email (or null)
- state: State abbreviation
- city: City name
- original_interest: What ${brand.shortName} originally discussed with them (1-2 sentences)
- last_contact_date: A realistic past date (YYYY-MM-DD format, between 2023-01-01 and 2025-06-30)
- warmth_score: 0.0-1.0 (how likely they'd respond to a rekindle, higher = warmer)
- ai_rekindle_reason: Why ${brand.shortName} should reach back out NOW (2-3 sentences, reference current events or policy changes)
- ai_approach: Suggested approach for re-engagement (1-2 sentences)
EXCLUDE these already-known names: ${existingNames.join(', ')}
Context — some of ${brand.shortName}'s current collaborations: ${collabs.rows.map(c => c.name + ' (' + c.collab_type + ')').join(', ')}
Recent ${brand.searchKeywords[0] || 'relevant'} news:
${newsRes.rows.map(n => '- ' + n.headline).join('\n')}
Return ONLY valid JSON: { "results": [ { name, contact_name, contact_title, organization, lead_type, email, state, city, original_interest, last_contact_date, warmth_score, ai_rekindle_reason, ai_approach } ] }`;
const geminiRes = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
temperature: 0.8,
maxOutputTokens: 4096,
responseMimeType: 'application/json',
},
}),
});
if (!geminiRes.ok) {
const errText = await geminiRes.text();
return NextResponse.json({ error: 'Gemini API error: ' + errText.slice(0, 200) }, { status: 502 });
}
const geminiData = await geminiRes.json();
const text = geminiData.candidates?.[0]?.content?.parts?.[0]?.text || '';
let results: Array<Record<string, unknown>> = [];
try {
const parsed = JSON.parse(text);
results = parsed.results || parsed;
} catch {
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 });
}
let inserted = 0;
for (const r of results) {
if (!r.name) continue;
const n = r.name as string;
if (existingNames.some(e => e.toLowerCase() === n.toLowerCase())) continue;
try {
await query(
`INSERT INTO old_leads (
lead_type, name, contact_name, contact_title, organization,
email, city, state, original_interest, last_contact_date,
warmth_score, ai_rekindle_reason, ai_approach, status
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,'cold')`,
[
r.lead_type || 'other',
n,
r.contact_name || null,
r.contact_title || null,
r.organization || null,
r.email || null,
r.city || null,
r.state || null,
r.original_interest || null,
r.last_contact_date || null,
r.warmth_score || 0.5,
r.ai_rekindle_reason || null,
r.ai_approach || null,
]
);
inserted++;
} catch {
// skip duplicates
}
}
return NextResponse.json({ success: true, discovered: results.length, inserted });
} catch (err) {
return NextResponse.json({ error: (err as Error).message }, { status: 500 });
}
}