← back to Norma

app/api/ingest/action-network/route.ts

127 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import {
  listPetitions,
  type ActionNetworkPetition,
} from '@/lib/action-network';

/**
 * POST /api/ingest/action-network
 *
 * Ingests petitions from Action Network.
 * Body: { pages?: number }
 *
 * Upserts into action_network_petitions via ON CONFLICT (an_id) DO UPDATE.
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  const stats = { ingested: 0, errors: [] as string[] };

  try {
    const body = await request.json().catch(() => ({}));
    const pages: number = Math.min(body.pages ?? 3, 20); // Cap at 20 pages

    // De-duplicate petitions across pages
    const allPetitions = new Map<string, ActionNetworkPetition>();

    for (let page = 1; page <= pages; page++) {
      try {
        const petitions = await listPetitions(page, 25);
        if (petitions.length === 0) break; // No more pages

        for (const petition of petitions) {
          if (petition.id && !allPetitions.has(petition.id)) {
            allPetitions.set(petition.id, petition);
          }
        }
      } catch (err) {
        stats.errors.push(`page_${page}: ${(err as Error).message}`);
      }
    }

    // Upsert each petition
    for (const petition of allPetitions.values()) {
      try {
        await query(
          `INSERT INTO action_network_petitions (
            an_id, title, description, petition_text, target,
            total_signatures, status, created_date, modified_date,
            url, raw_json, org_id
          ) VALUES (
            $1, $2, $3, $4, $5,
            $6, $7, $8, $9,
            $10, $11, $12
          )
          ON CONFLICT (an_id) DO UPDATE SET
            title = EXCLUDED.title,
            description = EXCLUDED.description,
            petition_text = EXCLUDED.petition_text,
            target = EXCLUDED.target,
            total_signatures = EXCLUDED.total_signatures,
            status = EXCLUDED.status,
            modified_date = EXCLUDED.modified_date,
            url = EXCLUDED.url,
            raw_json = EXCLUDED.raw_json,
            updated_at = NOW()`,
          [
            petition.id,
            petition.title,
            petition.description,
            petition.petitionText,
            petition.target,
            petition.totalSignatures,
            petition.status,
            petition.createdDate || null,
            petition.modifiedDate || null,
            petition.url,
            JSON.stringify(petition.rawJson),
            orgId,
          ],
        );
        stats.ingested++;
      } catch (err) {
        stats.errors.push(`insert_${petition.id}: ${(err as Error).message}`);
      }
    }
  } catch (err) {
    stats.errors.push('action_network_ingest: ' + (err as Error).message);
  }

  return NextResponse.json({
    success: true,
    ingested: stats.ingested,
    errors: stats.errors,
    message: `Ingested ${stats.ingested} Action Network petitions`,
  });
}

/* ─── GET: Return ingested Action Network petitions ──────────────────────── */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const { rows } = await query(`
      SELECT id, an_id, title, description, petition_text, target,
             total_signatures, status, created_date, modified_date,
             url, created_at, updated_at
      FROM action_network_petitions
      ORDER BY total_signatures DESC, modified_date DESC NULLS LAST
      LIMIT 200
    `);

    return NextResponse.json({
      petitions: rows,
      total: rows.length,
    });
  } catch (err) {
    console.error('[ingest/action-network] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch Action Network petitions' }, { status: 500 });
  }
}