← 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 });
  }
}