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 });
|
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({
|
||||||
|
|||||||
Reference in New Issue
Block a user