Files
NeoCom/src/app/api/customers/[customerId]/route.ts
2026-04-09 20:36:10 -07:00

148 lines
4.6 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/db";
import { getOrganization, apiError } from "@/lib/api-helpers";
export async function GET(
_request: NextRequest,
{ params }: { params: { customerId: string } }
) {
try {
const [organization, orgErr] = await getOrganization();
if (orgErr) return orgErr;
const customer = await prisma.customer.findFirst({
where: {
id: params.customerId,
organizationId: organization.id,
},
});
if (!customer) {
return NextResponse.json({ error: "Customer not found" }, { status: 404 });
}
// Fetch invoices and payments in parallel
const [invoices, payments] = await Promise.all([
prisma.invoice.findMany({
where: {
customerId: customer.id,
organizationId: organization.id,
},
select: {
id: true,
invoiceNumber: true,
invoiceDate: true,
dueDate: true,
status: true,
totalAmount: true,
paidAmount: true,
balanceDue: true,
currencyCode: true,
description: true,
},
orderBy: { invoiceDate: "desc" },
take: 50,
}),
prisma.payment.findMany({
where: {
customerId: customer.id,
organizationId: organization.id,
},
select: {
id: true,
paymentNumber: true,
paymentDate: true,
paymentMethod: true,
paymentAmount: true,
status: true,
paymentReference: true,
},
orderBy: { paymentDate: "desc" },
take: 20,
}),
]);
// Compute metrics
const totalInvoiced = invoices.reduce((s, i) => s + Number(i.totalAmount), 0);
const totalPaid = invoices.reduce((s, i) => s + Number(i.paidAmount), 0);
const totalOutstanding = invoices.reduce((s, i) => s + Number(i.balanceDue), 0);
const overdueInvoices = invoices.filter(
(i) => i.status !== "PAID" && i.status !== "VOID" && i.dueDate && new Date(i.dueDate) < new Date()
);
const overdueAmount = overdueInvoices.reduce((s, i) => s + Number(i.balanceDue), 0);
// Aging
const now = new Date();
const aging = { current: 0, days30: 0, days60: 0, days90: 0, over90: 0 };
invoices
.filter((i) => Number(i.balanceDue) > 0 && i.status !== "PAID" && i.status !== "VOID")
.forEach((i) => {
if (!i.dueDate) return;
const daysPast = Math.floor((now.getTime() - new Date(i.dueDate).getTime()) / 86400000);
const amt = Number(i.balanceDue);
if (daysPast <= 0) aging.current += amt;
else if (daysPast <= 30) aging.days30 += amt;
else if (daysPast <= 60) aging.days60 += amt;
else if (daysPast <= 90) aging.days90 += amt;
else aging.over90 += amt;
});
// Credit utilization
const creditLimit = customer.creditLimit ? Number(customer.creditLimit) : null;
const creditUtilization = creditLimit ? (totalOutstanding / creditLimit) * 100 : null;
return NextResponse.json({
customer: {
id: customer.id,
customerCode: customer.customerCode,
customerName: customer.customerName,
customerType: customer.customerType,
contactPerson: customer.contactPerson,
email: customer.email,
phone: customer.phone,
website: customer.website,
street1: customer.street1,
street2: customer.street2,
city: customer.city,
stateProvince: customer.stateProvince,
postalCode: customer.postalCode,
country: customer.country,
taxId: customer.taxId,
paymentTermDays: customer.paymentTermDays,
creditLimit,
status: customer.status,
createdAt: customer.createdAt,
},
metrics: {
totalInvoiced,
totalPaid,
totalOutstanding,
overdueAmount,
overdueCount: overdueInvoices.length,
invoiceCount: invoices.length,
paymentCount: payments.length,
creditLimit,
creditUtilization,
aging,
},
invoices: invoices.map((i) => ({
...i,
totalAmount: Number(i.totalAmount),
paidAmount: Number(i.paidAmount),
balanceDue: Number(i.balanceDue),
})),
payments: payments.map((p) => ({
id: p.id,
paymentNumber: p.paymentNumber,
paymentDate: p.paymentDate,
paymentMethod: p.paymentMethod,
amount: Number(p.paymentAmount),
status: p.status,
referenceNumber: p.paymentReference,
})),
});
} catch (error) {
return apiError("Failed to fetch customer details", error);
}
}