feat(core): add invoice update endpoint with validation
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user