← back to Norma

app/api/petitions/[id]/geo/route.ts

161 lines

/**
 * Petition Geo Targeting API
 * GET  /api/petitions/[id]/geo — geo breakdown (states, districts, top legislators)
 * POST /api/petitions/[id]/geo — auto-target legislators based on signature distribution
 *
 * Inspired by: UK Parliament's "Show on a map" feature
 */

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { updatePetitionGeoStats, getTargetLegislators } from '@/lib/petition-engine';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const auth = requireRole(req, 'admin', 'staff', 'pulse');
  if (auth instanceof NextResponse) return auth;

  const { id } = await params;
  const orgId = auth.role === 'admin' ? getOrgId(req) : auth.orgId;

  try {
    // Verify petition belongs to this org
    const { rowCount: petitionExists } = await query(
      `SELECT 1 FROM petitions WHERE id = $1 AND org_id = $2`,
      [id, orgId],
    );
    if (!petitionExists) {
      return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
    }

    // State-level breakdown
    const { rows: stateBreakdown } = await query(
      `SELECT state, SUM(signature_count) AS signatures,
              COUNT(DISTINCT district_code) AS districts
       FROM petition_geo_stats
       WHERE petition_id = $1
       GROUP BY state
       ORDER BY signatures DESC`,
      [id],
    );

    // District-level breakdown with legislator info
    const { rows: districtBreakdown } = await query(
      `SELECT gs.state, gs.district_code, gs.signature_count, gs.constituent_pct,
              gs.legislator_name, gs.legislator_party, gs.last_updated,
              p.email AS legislator_email, p.title AS legislator_title,
              p.committees, p.position_student_debt
       FROM petition_geo_stats gs
       LEFT JOIN politicians p ON p.congressional_district = gs.district_code
       WHERE gs.petition_id = $1 AND gs.district_code IS NOT NULL
       ORDER BY gs.signature_count DESC
       LIMIT 50`,
      [id],
    );

    // Petition totals
    const { rows: petitionRows } = await query(
      `SELECT title, total_signatures, target_states, target_districts
       FROM petitions WHERE id = $1 AND org_id = $2`,
      [id, orgId],
    );

    // Top 10 legislators to target
    const topLegislators = await getTargetLegislators(id, 10);

    return NextResponse.json({
      petition: petitionRows[0] || null,
      stateBreakdown: stateBreakdown.map((r) => ({
        state: r.state,
        signatures: Number(r.signatures),
        districts: Number(r.districts),
      })),
      districtBreakdown: districtBreakdown.map((r) => ({
        state: r.state,
        districtCode: r.district_code,
        signatureCount: Number(r.signature_count),
        constituentPct: r.constituent_pct ? Number(r.constituent_pct) : null,
        legislatorName: r.legislator_name,
        legislatorParty: r.legislator_party,
        legislatorEmail: r.legislator_email,
        legislatorTitle: r.legislator_title,
        committees: r.committees,
        positionStudentDebt: r.position_student_debt,
        lastUpdated: r.last_updated,
      })),
      topLegislators,
      totalStates: stateBreakdown.length,
      totalDistricts: districtBreakdown.length,
    });
  } catch (err) {
    console.error('[geo] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch geo data' }, { status: 500 });
  }
}

export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const auth = requireRole(req, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await params;
  const orgId = auth.role === 'admin' ? getOrgId(req) : auth.orgId;

  try {
    // Verify petition belongs to this org
    const { rowCount: petitionExists } = await query(
      `SELECT 1 FROM petitions WHERE id = $1 AND org_id = $2`,
      [id, orgId],
    );
    if (!petitionExists) {
      return NextResponse.json({ error: 'Petition not found' }, { status: 404 });
    }

    const body = await req.json();
    const { action } = body;

    if (action === 'refresh') {
      // Recalculate geo stats from signatures
      const breakdown = await updatePetitionGeoStats(id);
      return NextResponse.json({
        message: 'Geo stats refreshed',
        breakdown,
        totalStates: new Set(breakdown.map((b) => b.state)).size,
        totalDistricts: breakdown.filter((b) => b.districtCode).length,
      });
    }

    if (action === 'auto-target') {
      // Auto-set target states/districts based on signature distribution
      const topLegislators = await getTargetLegislators(id, 20);
      const targetStates = [...new Set(topLegislators.map((l) => l.state))];
      const targetDistricts = topLegislators
        .filter((l) => l.districtCode)
        .map((l) => l.districtCode);

      await query(
        `UPDATE petitions SET target_states = $2, target_districts = $3 WHERE id = $1 AND org_id = $4`,
        [id, targetStates, targetDistricts, orgId],
      );

      return NextResponse.json({
        message: 'Auto-targeting set',
        targetStates,
        targetDistricts,
        topLegislators,
      });
    }

    return NextResponse.json({ error: 'Unknown action. Use "refresh" or "auto-target".' }, { status: 400 });
  } catch (err) {
    console.error('[geo] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to process geo action' }, { status: 500 });
  }
}