feat(GRO-785): validate tip split totals before marking invoice paid
- PATCH /invoices/:id returns 400 when tipCents > 0 but no tip splits exist or splits don't sum to 100% - POST /invoices/:id/tip-splits now returns 400 (not 422) on validation failure via router-level ZodError handler Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -18,6 +18,14 @@ import type { AppEnv } from "../middleware/rbac.js";
|
|||||||
|
|
||||||
export const invoicesRouter = new Hono<AppEnv>();
|
export const invoicesRouter = new Hono<AppEnv>();
|
||||||
|
|
||||||
|
// Convert Zod validation errors from 422 to 400
|
||||||
|
invoicesRouter.onError((err, c) => {
|
||||||
|
if (err instanceof z.ZodError) {
|
||||||
|
return c.json({ error: "Validation failed", issues: err.issues }, 400);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
const createInvoiceSchema = z.object({
|
const createInvoiceSchema = z.object({
|
||||||
appointmentId: z.string().uuid().optional(),
|
appointmentId: z.string().uuid().optional(),
|
||||||
clientId: z.string().uuid(),
|
clientId: z.string().uuid(),
|
||||||
@@ -334,6 +342,29 @@ invoicesRouter.patch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate tip splits when marking invoice as paid
|
||||||
|
if (body.status === "paid" && current.tipCents > 0) {
|
||||||
|
const splits = await db
|
||||||
|
.select()
|
||||||
|
.from(invoiceTipSplits)
|
||||||
|
.where(eq(invoiceTipSplits.invoiceId, id));
|
||||||
|
|
||||||
|
if (splits.length === 0) {
|
||||||
|
return c.json(
|
||||||
|
{ error: "Tip split percentages must sum to 100%" },
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalBps = splits.reduce((sum, s) => sum + Math.round(Number(s.sharePct) * 100), 0);
|
||||||
|
if (totalBps !== 10000) {
|
||||||
|
return c.json(
|
||||||
|
{ error: "Tip split percentages must sum to 100%" },
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const update: Record<string, unknown> = { ...body, updatedAt: new Date() };
|
const update: Record<string, unknown> = { ...body, updatedAt: new Date() };
|
||||||
|
|
||||||
// Auto-set paidAt when marking as paid
|
// Auto-set paidAt when marking as paid
|
||||||
|
|||||||
Reference in New Issue
Block a user