initial commit
This commit is contained in:
147
src/app/api/customers/[customerId]/route.ts
Normal file
147
src/app/api/customers/[customerId]/route.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user