← back to Norma

app/api/clients/[id]/route.ts

153 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';

/**
 * GET /api/clients/:id
 * Fetch a single client record by ID.
 * Scoped to the requesting org_id.
 */
export async function GET(
  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 result = await query(
      `SELECT * FROM clients WHERE id = $1${orgId ? ' AND org_id = $2' : ''}`,
      orgId ? [id, orgId] : [id],
    );

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

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

/**
 * PATCH /api/clients/:id
 * Partial update — only the fields present in the request body are written.
 * Allowed fields are explicitly whitelisted to prevent mass-assignment.
 */
export async function PATCH(
  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 body = await request.json();

    const allowedFields = [
      'first_name', 'last_name', 'email', 'phone', 'city', 'state', 'zip',
      'client_type', 'status', 'priority',
      'case_notes', 'intake_date', 'assigned_to', 'tags',
      'debt_amount', 'debt_type', 'loan_servicer', 'repayment_plan',
      'last_contact_date', 'total_interactions', 'referral_source',
      'custom_fields',
    ];

    const updates: string[] = [];
    const values: unknown[] = [];
    let idx = 1;

    for (const [key, value] of Object.entries(body)) {
      if (allowedFields.includes(key)) {
        updates.push(`${key} = $${idx++}`);
        values.push(value);
      }
    }

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

    values.push(id);
    if (orgId) values.push(orgId);

    const result = await query(
      `UPDATE clients
       SET ${updates.join(', ')}, updated_at = NOW()
       WHERE id = $${idx}${orgId ? ` AND org_id = $${idx + 1}` : ''}
       RETURNING *`,
      values,
    );

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

    const client = result.rows[0];

    await auditLog(
      'client.updated',
      'client',
      id,
      { org_id: orgId, fields: Object.keys(body) },
      request.headers.get('x-forwarded-for') ?? undefined,
    );

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

/**
 * DELETE /api/clients/:id
 * Hard-delete a client record. Prefer setting status = 'archived' for soft deletes.
 * Scoped to the requesting org_id.
 */
export async function DELETE(
  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 result = await query(
      `DELETE FROM clients WHERE id = $1${orgId ? ' AND org_id = $2' : ''} RETURNING id, first_name, last_name`,
      orgId ? [id, orgId] : [id],
    );

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

    const deleted = result.rows[0];

    await auditLog(
      'client.deleted',
      'client',
      id,
      { org_id: orgId, name: `${deleted.first_name} ${deleted.last_name}` },
      request.headers.get('x-forwarded-for') ?? undefined,
    );

    return NextResponse.json({ deleted: true, id });
  } catch (err) {
    console.error('[api/clients/id] DELETE error:', (err as Error).message);
    return NextResponse.json({ error: 'Failed to delete client' }, { status: 500 });
  }
}