fix(GRO-785): address invoice tip split regression
- Use body.tipCents ?? current.tipCents for validation condition
so that simultaneous status=paid + tipCents=0 skip split validation
- Use body.tipCents (now aliased as tipCents) instead of current.tipCents
inside the atomic transaction for shareCents calculation
- Add explicit check for empty tipSplits array with appropriate error
message ("Tip splits are required when tip amount is greater than zero")
before the sum-to-100% check
- Destructure tipSplits out of body before spreading into update object
to prevent it from leaking into the invoices table SET clause
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -350,20 +350,25 @@ invoicesRouter.patch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate and persist tip splits when marking invoice as paid
|
// Validate and persist tip splits when marking invoice as paid
|
||||||
if (body.status === "paid" && current.tipCents > 0) {
|
const tipCents = body.tipCents ?? current.tipCents;
|
||||||
|
if (body.status === "paid" && tipCents > 0) {
|
||||||
// If incoming splits are provided in the request body, atomically replace them
|
// If incoming splits are provided in the request body, atomically replace them
|
||||||
if (body.tipSplits !== undefined) {
|
if (body.tipSplits !== undefined) {
|
||||||
|
if (body.tipSplits.length === 0) {
|
||||||
|
return c.json({ error: "Tip splits are required when tip amount is greater than zero" }, 400);
|
||||||
|
}
|
||||||
const totalPct = body.tipSplits.reduce((sum, s) => sum + s.sharePct, 0);
|
const totalPct = body.tipSplits.reduce((sum, s) => sum + s.sharePct, 0);
|
||||||
if (Math.abs(totalPct - 100) > 0.01) {
|
if (Math.abs(totalPct - 100) > 0.01) {
|
||||||
return c.json({ error: "Tip split percentages must sum to 100%" }, 400);
|
return c.json({ error: "Tip split percentages must sum to 100%" }, 400);
|
||||||
}
|
}
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx.delete(invoiceTipSplits).where(eq(invoiceTipSplits.invoiceId, id));
|
await tx.delete(invoiceTipSplits).where(eq(invoiceTipSplits.invoiceId, id));
|
||||||
if (body.tipSplits.length > 0) {
|
const splits = body.tipSplits!;
|
||||||
let remaining = current.tipCents;
|
if (splits.length > 0) {
|
||||||
const rows = body.tipSplits.map((s, i) => {
|
let remaining = tipCents;
|
||||||
const isLast = i === body.tipSplits.length - 1;
|
const rows = splits.map((s, i) => {
|
||||||
const shareCents = isLast ? remaining : Math.round((s.sharePct / 100) * current.tipCents);
|
const isLast = i === splits.length - 1;
|
||||||
|
const shareCents = isLast ? remaining : Math.round((s.sharePct / 100) * tipCents);
|
||||||
if (!isLast) remaining -= shareCents;
|
if (!isLast) remaining -= shareCents;
|
||||||
return {
|
return {
|
||||||
invoiceId: id,
|
invoiceId: id,
|
||||||
@@ -400,7 +405,10 @@ invoicesRouter.patch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const update: Record<string, unknown> = { ...body, updatedAt: new Date() };
|
// Destructure tipSplits out — it belongs to a separate table, not the invoices column
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { tipSplits: _tipSplits, ...updateBody } = body as Record<string, unknown>;
|
||||||
|
const update: Record<string, unknown> = { ...updateBody, updatedAt: new Date() };
|
||||||
|
|
||||||
// Auto-set paidAt when marking as paid
|
// Auto-set paidAt when marking as paid
|
||||||
if (body.status === "paid" && !body.paidAt && !current.paidAt) {
|
if (body.status === "paid" && !body.paidAt && !current.paidAt) {
|
||||||
|
|||||||
Reference in New Issue
Block a user