← back to Freddy
app/api/contacts/discover/route.ts
142 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { verifyAuth } from '@/lib/auth';
import { auditLog } from '@/lib/audit';
const GEMINI_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=' + (process.env.GEMINI_API_KEY || '');
export async function POST(request: NextRequest) {
const user = verifyAuth(request);
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
// Fetch context: top causes, orgs, and granters
const [causesRes, orgsRes, grantersRes] = await Promise.all([
query(`SELECT title, category, urgency_score FROM causes WHERE status = 'active' ORDER BY urgency_score DESC LIMIT 10`),
query(`SELECT name, category, mission FROM organizations WHERE status IN ('verified', 'featured') LIMIT 10`),
query(`SELECT name, funding_areas FROM granters WHERE status = 'active' LIMIT 10`),
]);
const causesContext = causesRes.rows.map(c => c.title).join(', ');
const orgsContext = orgsRes.rows.map(o => o.name).join(', ');
const grantersContext = grantersRes.rows.map(g => g.name).join(', ');
const prompt = `You are a fundraising consultant specializing in connecting non-profits with grant funders. Generate 8-12 realistic fictional contacts for a fundraising coordination platform.
CONTEXT:
- Active causes: ${causesContext || 'education, health, poverty, environment'}
- Registered orgs: ${orgsContext || 'various non-profits'}
- Active granters: ${grantersContext || 'various foundations'}
Generate contacts that include:
- Non-profit executive directors and program directors
- Foundation grant officers and program managers
- Board members of funding organizations
- Government grant coordinators
For each contact, provide:
- name: Full name (realistic)
- title: Job title
- organization: Their organization name
- contact_type: One of: nonprofit_leader, grant_officer, board_member, program_director, government, media
- email: Professional email address
- phone: Phone number (xxx-xxx-xxxx format)
- city: City
- state: State abbreviation
- relevance_score: 0.0-1.0 (how relevant to our causes)
- ai_pitch: A 1-2 sentence suggestion for how to approach this person
Return ONLY valid JSON array, no markdown:
[{"name":"...", "title":"...", "organization":"...", "contact_type":"...", "email":"...", "phone":"...", "city":"...", "state":"...", "relevance_score":0.85, "ai_pitch":"..."}]`;
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 },
}),
});
if (!geminiRes.ok) {
const errText = await geminiRes.text();
console.error('[contacts/discover] Gemini error:', errText);
return NextResponse.json({ error: 'AI service error' }, { status: 502 });
}
const geminiData = await geminiRes.json();
const rawText = geminiData?.candidates?.[0]?.content?.parts?.[0]?.text || '';
let jsonStr = rawText.trim();
if (jsonStr.startsWith('```')) {
jsonStr = jsonStr.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
}
let contacts: Array<{
name: string;
title: string;
organization: string;
contact_type: string;
email: string;
phone: string;
city: string;
state: string;
relevance_score: number;
ai_pitch: string;
}>;
try {
contacts = JSON.parse(jsonStr);
} catch {
console.error('[contacts/discover] Parse error:', jsonStr.slice(0, 300));
return NextResponse.json({ error: 'Failed to parse AI response' }, { status: 500 });
}
const validTypes = ['nonprofit_leader', 'grant_officer', 'board_member', 'program_director', 'government', 'media', 'other'];
const inserted = [];
for (const contact of contacts) {
const contactType = validTypes.includes(contact.contact_type) ? contact.contact_type : 'other';
try {
const result = await query(
`INSERT INTO contacts (contact_type, name, title, organization, email, phone, city, state, source, relevance_score, ai_pitch)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'ai_discovery', $9, $10)
RETURNING *`,
[
contactType,
contact.name,
contact.title || null,
contact.organization || null,
contact.email || null,
contact.phone || null,
contact.city || null,
contact.state || null,
Math.min(1, Math.max(0, contact.relevance_score || 0.5)),
contact.ai_pitch || null,
],
);
inserted.push(result.rows[0]);
} catch (insertErr) {
console.error('[contacts/discover] Insert error for:', contact.name, (insertErr as Error).message);
}
}
await auditLog('contacts.discovered', 'contact', null, {
user,
discovered: contacts.length,
inserted: inserted.length,
});
return NextResponse.json({
discovered: contacts.length,
inserted: inserted.length,
contacts: inserted,
});
} catch (err) {
console.error('[contacts/discover] Error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to discover contacts' }, { status: 500 });
}
}