148 lines
4.6 KiB
TypeScript
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);
|
|
}
|
|
}
|