← back to Norma

app/api/geo/orchestrate/route.ts

91 lines

/**
 * POST /api/geo/orchestrate
 *
 * Master orchestrator that runs all geo-profiling sub-agents in sequence:
 *   1. buildZipProfiles()  — create composite zip profiles
 *   2. detectHotspots()    — flag statistical outliers
 *   3. computeAdvocacyScores() — score zips for campaign targeting
 *
 * Returns combined results with timing for each step.
 * Requires authentication.
 */

import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import {
  buildZipProfiles,
  detectHotspots,
  computeAdvocacyScores,
  type ProfilerResult,
} from '@/lib/geo-profiler';

export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const overallStart = Date.now();
  const results: Record<string, ProfilerResult> = {};
  const stepTimings: { step: string; elapsed: string; status: string }[] = [];

  try {
    // ── Step 1: Build zip profiles ──
    const profileResult = await buildZipProfiles();
    results.buildZipProfiles = profileResult;
    stepTimings.push({
      step: 'buildZipProfiles',
      elapsed: profileResult.elapsed,
      status: profileResult.errors === 0 ? 'success' : 'partial',
    });

    // ── Step 2: Detect hotspots ──
    const hotspotResult = await detectHotspots();
    results.detectHotspots = hotspotResult;
    stepTimings.push({
      step: 'detectHotspots',
      elapsed: hotspotResult.elapsed,
      status: hotspotResult.errors === 0 ? 'success' : 'partial',
    });

    // ── Step 3: Compute advocacy scores ──
    const advocacyResult = await computeAdvocacyScores();
    results.computeAdvocacyScores = advocacyResult;
    stepTimings.push({
      step: 'computeAdvocacyScores',
      elapsed: advocacyResult.elapsed,
      status: advocacyResult.errors === 0 ? 'success' : 'partial',
    });

    const overallElapsed = `${((Date.now() - overallStart) / 1000).toFixed(1)}s`;
    const totalProcessed = Object.values(results).reduce((sum, r) => sum + r.processed, 0);
    const totalErrors = Object.values(results).reduce((sum, r) => sum + r.errors, 0);

    return NextResponse.json({
      success: true,
      overall: {
        total_processed: totalProcessed,
        total_errors: totalErrors,
        elapsed: overallElapsed,
        status: totalErrors === 0 ? 'success' : 'partial',
      },
      steps: stepTimings,
      details: results,
      triggered_by: auth.username,
      completed_at: new Date().toISOString(),
    });
  } catch (err) {
    const overallElapsed = `${((Date.now() - overallStart) / 1000).toFixed(1)}s`;
    console.error('[geo/orchestrate] Pipeline error:', (err as Error).message);

    return NextResponse.json(
      {
        success: false,
        error: `Pipeline failed: ${(err as Error).message}`,
        elapsed: overallElapsed,
        steps: stepTimings,
        details: results,
      },
      { status: 500 },
    );
  }
}