feat(core): add invoice update endpoint with validation

This commit is contained in:
Prad Nukala
2026-08-10 11:01:23 -04:00
parent 3504fc575d
commit a5fad4fb20
+42
View File
@@ -50,6 +50,48 @@ app.get("/invoice/:id", (c) => {
return c.json({ invoice, payments }); 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) ----------------------- // --- Payments (happy path: pay an invoice, idempotent) -----------------------
const CreatePayment = z.object({ const CreatePayment = z.object({