← 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 });
}
}