From a5fad4fb201623207e4dada423a6aa7056f01752 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Mon, 10 Aug 2026 11:01:23 -0400 Subject: [PATCH] feat(core): add invoice update endpoint with validation --- src/index.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/index.ts b/src/index.ts index f18df01..865b9de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,48 @@ app.get("/invoice/:id", (c) => { return c.json({ invoice, payments }); }); +// Identity fields (id, invoice_number, customer_id, created_at) are immutable; +// strictObject rejects them explicitly instead of silently stripping. +const UpdateInvoice = z + .strictObject({ + status: z.enum(["open", "partially_paid", "paid", "void", "corrupted"]), + due_on: z.iso.date(), + description: z.string().nullable(), + quantity: z.number().int().positive(), + unit_price: z.number().int().nonnegative(), + amount: z.number().int().nonnegative(), + currency: z.string().min(1).transform((s) => s.toUpperCase()), + }) + .partial() + .refine((o) => Object.keys(o).length > 0, { error: "provide at least one updatable field" }); + +app.put("/invoice/:id", async (c) => { + const parsed = UpdateInvoice.safeParse(await c.req.json().catch(() => null)); + if (!parsed.success) return c.json({ error: z.treeifyError(parsed.error) }, 400); + const key = c.req.param("id"); + + const target = db + .query<{ id: string }, [string, string]>( + "SELECT id FROM invoices WHERE id = ? OR invoice_number = ? ORDER BY created_at DESC", + ) + .get(key, key); + if (!target) return c.json({ error: "invoice not found" }, 404); + + // Keys are whitelisted by the zod schema, so interpolating them is safe + const fields = Object.entries(parsed.data); + try { + db.run( + `UPDATE invoices SET ${fields.map(([k]) => `${k} = ?`).join(", ")} WHERE id = ?`, + [...fields.map(([, v]) => v), target.id], + ); + } catch (err) { + // schema CHECK: any non-corrupted status requires all money/date fields present — + // repairing a corrupted invoice means supplying the missing fields in the same PUT + return c.json({ error: err instanceof Error ? err.message : String(err) }, 400); + } + return c.json(db.query("SELECT * FROM invoices WHERE id = ?").get(target.id)); +}); + // --- Payments (happy path: pay an invoice, idempotent) ----------------------- const CreatePayment = z.object({