← back to Norma

app/api/donations/insights/[id]/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 { auditLog } from '@/lib/audit';

type RouteContext = { params: Promise<{ id: string }> };

/**
 * PATCH /api/donations/insights/[id]
 * Update an insight (primarily for is_dismissed toggle).
 */
export async function PATCH(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await context.params;

  try {
    const body = await request.json();
    const allowedFields = [
      'insight_type', 'title', 'description', 'data_points',
      'confidence', 'is_actionable', 'is_dismissed',
    ];

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

    for (const field of allowedFields) {
      if (field in body) {
        // data_points needs JSON stringification
        if (field === 'data_points' && body[field] !== null && typeof body[field] === 'object') {
          setClauses.push(`${field} = $${paramIndex}`);
          values.push(JSON.stringify(body[field]));
        } else {
          setClauses.push(`${field} = $${paramIndex}`);
          values.push(body[field]);
        }
        paramIndex++;
      }
    }

    if (setClauses.length === 0) {
      return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
    }

    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    values.push(id);
    let sql = `UPDATE donation_insights SET ${setClauses.join(', ')} WHERE id = $${paramIndex}`;
    if (orgId) {
      values.push(orgId);
      sql += ` AND org_id = $${paramIndex + 1}`;
    }
    sql += ' RETURNING *';
    const result = await query(sql, values);

    if (result.rowCount === 0) {
      return NextResponse.json({ error: 'Insight not found' }, { status: 404 });
    }

    const ip =
      request.headers.get('x-forwarded-for') ||
      request.headers.get('x-real-ip') ||
      undefined;
    await auditLog(
      'donation_insight.updated',
      'donation_insight',
      id,
      { fields: Object.keys(body).filter((k) => allowedFields.includes(k)) },
      ip
    );

    return NextResponse.json({ insight: result.rows[0] });
  } catch (err) {
    console.error('[api/donations/insights/[id]] PATCH error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to update insight' }, { status: 500 });
  }
}

/**
 * DELETE /api/donations/insights/[id]
 * Delete a donation insight.
 */
export async function DELETE(request: NextRequest, context: RouteContext) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { id } = await context.params;

  try {
    const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
    const delParams: unknown[] = [id];
    let delSql = `DELETE FROM donation_insights WHERE id = $1`;
    if (orgId) {
      delParams.push(orgId);
      delSql += ` AND org_id = $2`;
    }
    delSql += ' RETURNING id, insight_type, title';
    const result = await query(delSql, delParams);

    if (result.rowCount === 0) {
      return NextResponse.json({ error: 'Insight not found' }, { status: 404 });
    }

    const ip =
      request.headers.get('x-forwarded-for') ||
      request.headers.get('x-real-ip') ||
      undefined;
    await auditLog(
      'donation_insight.deleted',
      'donation_insight',
      id,
      { insight_type: result.rows[0].insight_type, title: result.rows[0].title },
      ip
    );

    return NextResponse.json({ success: true, deleted: result.rows[0] });
  } catch (err) {
    console.error('[api/donations/insights/[id]] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete insight' }, { status: 500 });
  }
}