← back to Norma

app/api/pipeline/[id]/approve/route.ts

91 lines

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

/**
 * POST /api/pipeline/[id]/approve
 * Approve a pipeline item for posting.
 * Sets status='approved', reviewed_by, reviewed_at.
 * Optionally accepts { review_notes } in body.
 */
export async function POST(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

  try {
    const { id } = await params;

    const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
    if (!uuidRegex.test(id)) {
      return NextResponse.json({ error: 'Invalid ID format' }, { status: 400 });
    }

    // Check current status
    const existing = await query(
      `SELECT id, status FROM petition_pipeline WHERE id = $1 AND ($2::text IS NULL OR org_id = $2)`,
      [id, orgId]
    );

    if (existing.rows.length === 0) {
      return NextResponse.json({ error: 'Pipeline item not found' }, { status: 404 });
    }

    const currentStatus = existing.rows[0].status;
    if (currentStatus !== 'draft') {
      return NextResponse.json(
        { error: `Cannot approve item with status '${currentStatus}'. Only draft items can be approved.` },
        { status: 409 }
      );
    }

    // Parse optional review notes
    let reviewNotes: string | null = null;
    try {
      const body = await request.json();
      if (body.review_notes && typeof body.review_notes === 'string') {
        reviewNotes = body.review_notes.trim() || null;
      }
    } catch {
      // Body is optional for this endpoint
    }

    const result = await query(
      `UPDATE petition_pipeline
       SET status = 'approved',
           reviewed_by = $1,
           reviewed_at = NOW(),
           review_notes = COALESCE($2, review_notes),
           updated_at = NOW()
       WHERE id = $3 AND ($4::text IS NULL OR org_id = $4)
       RETURNING *`,
      [auth.username, reviewNotes, id, orgId]
    );

    const item = result.rows[0];

    const ip =
      request.headers.get('x-forwarded-for') ||
      request.headers.get('x-real-ip') ||
      undefined;
    await auditLog(
      'pipeline.approved',
      'petition_pipeline',
      item.id,
      { reviewed_by: auth.username, title: item.title },
      ip
    );

    return NextResponse.json({ item });
  } catch (err) {
    console.error('[api/pipeline/[id]/approve] error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to approve pipeline item' }, { status: 500 });
  }
}