← back to Norma
app/api/ingest/congress-gov/route.ts
124 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import { searchBills, type Bill } from '@/lib/congress-gov';
/**
* POST /api/ingest/congress-gov
*
* Ingests federal bills from Congress.gov API.
* Body: { keywords?: string[], congress?: number }
* Upserts into congress_bills table.
*/
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;
let body: { keywords?: string[]; congress?: number } = {};
try {
body = await request.json();
} catch {
/* use defaults */
}
const keywords = body.keywords ?? ['education', 'nonprofit', 'public health'];
const congress = body.congress ?? undefined;
const stats = { ingested: 0, errors: 0 };
const errorDetails: string[] = [];
// Deduplicate across keyword searches
const allBills = new Map<string, Bill>();
for (const keyword of keywords) {
try {
console.log('[ingest/congress-gov] Searching bills for:', keyword);
const bills = await searchBills(keyword, congress, 50);
for (const bill of bills) {
const key = `${bill.congress}-${bill.billType}-${bill.billNumber}`;
if (!allBills.has(key)) {
allBills.set(key, bill);
}
}
} catch (err) {
console.error('[ingest/congress-gov] Search error for keyword:', keyword, (err as Error).message);
errorDetails.push('search_' + keyword + ': ' + (err as Error).message);
stats.errors++;
}
}
console.log('[ingest/congress-gov] Found', allBills.size, 'unique bills to upsert');
for (const bill of allBills.values()) {
try {
await query(
`INSERT INTO congress_bills (
congress, bill_type, bill_number, title,
introduced_date, latest_action_text, latest_action_date,
policy_area, url, raw_json, org_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (congress, bill_type, bill_number) DO UPDATE SET
title = EXCLUDED.title,
latest_action_text = EXCLUDED.latest_action_text,
latest_action_date = EXCLUDED.latest_action_date,
policy_area = EXCLUDED.policy_area,
url = EXCLUDED.url,
raw_json = EXCLUDED.raw_json,
updated_at = NOW()`,
[
bill.congress,
bill.billType,
bill.billNumber,
bill.title,
bill.introducedDate || null,
bill.latestActionText || null,
bill.latestActionDate || null,
bill.policyArea || null,
bill.url || null,
JSON.stringify(bill),
orgId || null,
],
);
stats.ingested++;
} catch (err) {
console.error('[ingest/congress-gov] Insert error:', (err as Error).message);
errorDetails.push('insert: ' + (err as Error).message);
stats.errors++;
}
}
console.log('[ingest/congress-gov] Done:', stats.ingested, 'ingested,', stats.errors, 'errors');
return NextResponse.json({
success: true,
ingested: stats.ingested,
errors: stats.errors,
...(errorDetails.length > 0 ? { errorDetails } : {}),
});
}
/* ─── GET: Return ingested congress bills ────────────────────────────────── */
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, congress, bill_type, bill_number, title,
introduced_date, latest_action_text, latest_action_date,
sponsor_name, policy_area, url, status, created_at
FROM congress_bills
ORDER BY latest_action_date DESC NULLS LAST, created_at DESC
LIMIT 200
`);
return NextResponse.json({ bills: rows, total: rows.length });
} catch (err) {
console.error('[ingest/congress-gov] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch congress bills' }, { status: 500 });
}
}