fix(GRO-785): restore atomic tip split save in PATCH and fix error message
- When body.tipSplits is provided in PATCH /invoices/:id, validate sum first then atomically replace existing splits (delete + insert) - When no incoming splits, validate existing DB splits with corrected message: "Tip splits are required when tip amount is greater than zero" (previously misleading "must sum to 100%" when no splits existed) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -349,26 +349,54 @@ invoicesRouter.patch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tip splits when marking invoice as paid
|
// Validate and persist tip splits when marking invoice as paid
|
||||||
if (body.status === "paid" && current.tipCents > 0) {
|
if (body.status === "paid" && current.tipCents > 0) {
|
||||||
const splits = await db
|
// If incoming splits are provided in the request body, atomically replace them
|
||||||
.select()
|
if (body.tipSplits !== undefined) {
|
||||||
.from(invoiceTipSplits)
|
const totalPct = body.tipSplits.reduce((sum, s) => sum + s.sharePct, 0);
|
||||||
.where(eq(invoiceTipSplits.invoiceId, id));
|
if (Math.abs(totalPct - 100) > 0.01) {
|
||||||
|
return c.json({ error: "Tip split percentages must sum to 100%" }, 400);
|
||||||
|
}
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.delete(invoiceTipSplits).where(eq(invoiceTipSplits.invoiceId, id));
|
||||||
|
if (body.tipSplits.length > 0) {
|
||||||
|
let remaining = current.tipCents;
|
||||||
|
const rows = body.tipSplits.map((s, i) => {
|
||||||
|
const isLast = i === body.tipSplits.length - 1;
|
||||||
|
const shareCents = isLast ? remaining : Math.round((s.sharePct / 100) * current.tipCents);
|
||||||
|
if (!isLast) remaining -= shareCents;
|
||||||
|
return {
|
||||||
|
invoiceId: id,
|
||||||
|
staffId: s.staffId,
|
||||||
|
staffName: s.staffName,
|
||||||
|
sharePct: s.sharePct.toFixed(2),
|
||||||
|
shareCents,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await tx.insert(invoiceTipSplits).values(rows);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No incoming splits — validate existing DB splits
|
||||||
|
const splits = await db
|
||||||
|
.select()
|
||||||
|
.from(invoiceTipSplits)
|
||||||
|
.where(eq(invoiceTipSplits.invoiceId, id));
|
||||||
|
|
||||||
if (splits.length === 0) {
|
if (splits.length === 0) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Tip split percentages must sum to 100%" },
|
{ error: "Tip splits are required when tip amount is greater than zero" },
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalBps = splits.reduce((sum, s) => sum + Math.round(Number(s.sharePct) * 100), 0);
|
const totalBps = splits.reduce((sum, s) => sum + Math.round(Number(s.sharePct) * 100), 0);
|
||||||
if (totalBps !== 10000) {
|
if (totalBps !== 10000) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ error: "Tip split percentages must sum to 100%" },
|
{ error: "Tip split percentages must sum to 100%" },
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user