From 369c2ce18230c862390e613b10af4140046d125b Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 14 Apr 2026 13:59:50 +0000 Subject: [PATCH] fix(invoices): add Zod query param validation to GET / --- apps/api/src/routes/invoices.ts | 98 ++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/apps/api/src/routes/invoices.ts b/apps/api/src/routes/invoices.ts index 96c3e0e..0f65a34 100644 --- a/apps/api/src/routes/invoices.ts +++ b/apps/api/src/routes/invoices.ts @@ -44,53 +44,61 @@ const updateInvoiceSchema = z.object({ }); // List invoices -invoicesRouter.get("/", async (c) => { - const db = getDb(); - const clientId = c.req.query("clientId"); - const appointmentId = c.req.query("appointmentId"); - const status = c.req.query("status"); - const limit = Math.min(parseInt(c.req.query("limit") || "50", 10), 200); - const offset = parseInt(c.req.query("offset") || "0", 10); - - const conditions = []; - if (clientId) conditions.push(eq(invoices.clientId, clientId)); - if (appointmentId) conditions.push(eq(invoices.appointmentId, appointmentId)); - if (status) conditions.push(eq(invoices.status, status as "draft" | "pending" | "paid" | "void")); - - const whereClause = conditions.length > 0 ? and(...conditions) : undefined; - - const [totalResult] = await db - .select({ count: sql`count(*)` }) - .from(invoices) - .where(whereClause); - - const rows = await db - .select({ - id: invoices.id, - appointmentId: invoices.appointmentId, - clientId: invoices.clientId, - clientName: clients.name, - subtotalCents: invoices.subtotalCents, - taxCents: invoices.taxCents, - tipCents: invoices.tipCents, - totalCents: invoices.totalCents, - status: invoices.status, - paymentMethod: invoices.paymentMethod, - paidAt: invoices.paidAt, - notes: invoices.notes, - createdAt: invoices.createdAt, - updatedAt: invoices.updatedAt, - }) - .from(invoices) - .leftJoin(clients, eq(invoices.clientId, clients.id)) - .where(whereClause) - .orderBy(invoices.createdAt) - .limit(limit) - .offset(offset); - - return c.json({ data: rows, total: totalResult?.count ?? 0 }); +const listInvoicesQuerySchema = z.object({ + clientId: z.string().uuid().optional(), + appointmentId: z.string().uuid().optional(), + status: z.enum(["draft", "pending", "paid", "void"]).optional(), + limit: z.coerce.number().int().min(1).max(200).default(50), + offset: z.coerce.number().int().min(0).default(0), }); +invoicesRouter.get( + "/", + zValidator("query", listInvoicesQuerySchema), + async (c) => { + const db = getDb(); + const { clientId, appointmentId, status, limit, offset } = c.req.valid("query"); + + const conditions = []; + if (clientId) conditions.push(eq(invoices.clientId, clientId)); + if (appointmentId) conditions.push(eq(invoices.appointmentId, appointmentId)); + if (status) conditions.push(eq(invoices.status, status as "draft" | "pending" | "paid" | "void")); + + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + const [totalResult] = await db + .select({ count: sql`count(*)` }) + .from(invoices) + .where(whereClause); + + const rows = await db + .select({ + id: invoices.id, + appointmentId: invoices.appointmentId, + clientId: invoices.clientId, + clientName: clients.name, + subtotalCents: invoices.subtotalCents, + taxCents: invoices.taxCents, + tipCents: invoices.tipCents, + totalCents: invoices.totalCents, + status: invoices.status, + paymentMethod: invoices.paymentMethod, + paidAt: invoices.paidAt, + notes: invoices.notes, + createdAt: invoices.createdAt, + updatedAt: invoices.updatedAt, + }) + .from(invoices) + .leftJoin(clients, eq(invoices.clientId, clients.id)) + .where(whereClause) + .orderBy(invoices.createdAt) + .limit(limit) + .offset(offset); + + return c.json({ data: rows, total: totalResult?.count ?? 0 }); + } +); + // Get single invoice with line items and tip splits invoicesRouter.get("/:id", async (c) => { const db = getDb();