← back to Norma

app/api/donations/insights/route.ts

130 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { auditLog } from '@/lib/audit';

/**
 * GET /api/donations/insights
 * List donation insights.
 * Filters: ?type= (trend, recommendation, warning, milestone),
 *          ?is_actionable= (true/false),
 *          ?is_dismissed= (true/false)
 * Order by confidence DESC, created_at DESC.
 * Returns { insights: [...] }
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const { searchParams } = new URL(request.url);
    const type = searchParams.get('type');
    const isActionable = searchParams.get('is_actionable');
    const isDismissed = searchParams.get('is_dismissed');

    const conditions: string[] = [];
    const values: unknown[] = [];
    let paramIndex = 1;

    if (type) {
      conditions.push(`insight_type = $${paramIndex}`);
      values.push(type);
      paramIndex++;
    }

    if (isActionable !== null && isActionable !== undefined && isActionable !== '') {
      conditions.push(`is_actionable = $${paramIndex}`);
      values.push(isActionable === 'true');
      paramIndex++;
    }

    if (isDismissed !== null && isDismissed !== undefined && isDismissed !== '') {
      conditions.push(`is_dismissed = $${paramIndex}`);
      values.push(isDismissed === 'true');
      paramIndex++;
    }

    const whereClause =
      conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

    const result = await query(
      `SELECT * FROM donation_insights ${whereClause}
       ORDER BY confidence DESC NULLS LAST, created_at DESC`,
      values
    );

    return NextResponse.json({ insights: result.rows });
  } catch (err) {
    console.error('[api/donations/insights] GET error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to fetch donation insights' }, { status: 500 });
  }
}

/**
 * POST /api/donations/insights
 * Add a donation insight manually.
 * Body: { insight_type, title, description, data_points?, confidence?, is_actionable? }
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  try {
    const body = await request.json();

    if (!body.insight_type || typeof body.insight_type !== 'string') {
      return NextResponse.json({ error: 'insight_type is required' }, { status: 400 });
    }

    const validTypes = ['trend', 'recommendation', 'warning', 'milestone'];
    if (!validTypes.includes(body.insight_type)) {
      return NextResponse.json(
        { error: `insight_type must be one of: ${validTypes.join(', ')}` },
        { status: 400 }
      );
    }

    if (!body.title || typeof body.title !== 'string' || body.title.trim() === '') {
      return NextResponse.json({ error: 'title is required' }, { status: 400 });
    }

    if (!body.description || typeof body.description !== 'string' || body.description.trim() === '') {
      return NextResponse.json({ error: 'description is required' }, { status: 400 });
    }

    const result = await query(
      `INSERT INTO donation_insights
         (insight_type, title, description, data_points, confidence, is_actionable)
       VALUES ($1, $2, $3, $4, $5, $6)
       RETURNING *`,
      [
        body.insight_type,
        body.title.trim(),
        body.description.trim(),
        body.data_points ? JSON.stringify(body.data_points) : null,
        body.confidence != null ? Number(body.confidence) : null,
        body.is_actionable !== false, // defaults to true
      ]
    );

    const insight = result.rows[0];

    const ip =
      request.headers.get('x-forwarded-for') ||
      request.headers.get('x-real-ip') ||
      undefined;
    await auditLog(
      'donation_insight.created',
      'donation_insight',
      insight.id,
      { insight_type: insight.insight_type, title: insight.title },
      ip
    );

    return NextResponse.json({ insight }, { status: 201 });
  } catch (err) {
    console.error('[api/donations/insights] POST error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to create donation insight' }, { status: 500 });
  }
}