← back to Norma
app/api/donations/[id]/route.ts
142 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';
type RouteContext = { params: Promise<{ id: string }> };
/**
* GET /api/donations/[id]
* Return a single donation by ID.
*/
export async function GET(request: NextRequest, context: RouteContext) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const { id } = await context.params;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
const orgFilter = orgId ? ' AND org_id = $2' : '';
const result = await query(`SELECT * FROM donations WHERE id = $1${orgFilter}`, orgId ? [id, orgId] : [id]);
if (result.rowCount === 0) {
return NextResponse.json({ error: 'Donation not found' }, { status: 404 });
}
return NextResponse.json({ donation: result.rows[0] });
} catch (err) {
console.error('[api/donations/[id]] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch donation' }, { status: 500 });
}
}
/**
* PATCH /api/donations/[id]
* Update donation fields.
*/
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;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
const body = await request.json();
const allowedFields = [
'source', 'amount', 'donor_type', 'campaign_name', 'petition_id',
'draft_id', 'channel', 'recurring', 'notes', 'tags', 'donated_at',
];
const setClauses: string[] = [];
const values: unknown[] = [];
let paramIndex = 1;
for (const field of allowedFields) {
if (field in body) {
setClauses.push(`${field} = $${paramIndex}`);
values.push(body[field]);
paramIndex++;
}
}
if (setClauses.length === 0) {
return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 });
}
values.push(id);
paramIndex++;
if (orgId) {
values.push(orgId);
}
const result = await query(
`UPDATE donations SET ${setClauses.join(', ')} WHERE id = $${paramIndex - 1}${orgId ? ` AND org_id = $${paramIndex}` : ''} RETURNING *`,
values
);
if (result.rowCount === 0) {
return NextResponse.json({ error: 'Donation not found' }, { status: 404 });
}
const ip =
request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
undefined;
await auditLog(
'donation.updated',
'donation',
id,
{ fields: Object.keys(body).filter((k) => allowedFields.includes(k)) },
ip
);
return NextResponse.json({ donation: result.rows[0] });
} catch (err) {
console.error('[api/donations/[id]] PATCH error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to update donation' }, { status: 500 });
}
}
/**
* DELETE /api/donations/[id]
* Delete a donation.
*/
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;
const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
try {
const orgFilter = orgId ? ' AND org_id = $2' : '';
const result = await query(
`DELETE FROM donations WHERE id = $1${orgFilter} RETURNING id, source, amount, campaign_name`,
orgId ? [id, orgId] : [id]
);
if (result.rowCount === 0) {
return NextResponse.json({ error: 'Donation not found' }, { status: 404 });
}
const ip =
request.headers.get('x-forwarded-for') ||
request.headers.get('x-real-ip') ||
undefined;
await auditLog(
'donation.deleted',
'donation',
id,
{ amount: result.rows[0].amount, source: result.rows[0].source },
ip
);
return NextResponse.json({ success: true, deleted: result.rows[0] });
} catch (err) {
console.error('[api/donations/[id]] DELETE error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to delete donation' }, { status: 500 });
}
}