fix(GRO-751): add server-side tip split validation to markPaid

- Extend updateInvoiceSchema to accept optional tipSplits array in PATCH body
- Validate tip splits sum to 100% (10000 bps) when marking paid with tipCents > 0
- Return 422 if tipSplits not provided and no existing splits in DB
- Save tip splits atomically in same DB transaction as invoice status update
- Update frontend markPaid() to send tipSplits in PATCH body instead of separate POST
- Remove non-atomic POST /tip-splits call from markPaid flow

Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit was merged in pull request #320.
This commit is contained in:
the-dogfather-cto[bot]
2026-04-17 12:33:43 +00:00
committed by GitHub
parent b683c57d6c
commit 3c7820d785
2 changed files with 82 additions and 26 deletions
+16 -20
View File
@@ -221,35 +221,31 @@ function InvoiceDetailModal({
}
}
try {
const patchBody: {
status: string;
paymentMethod: string;
tipCents: number;
tipSplits?: Array<{ staffId: string | null; staffName: string; sharePct: number }>;
} = { status: "paid", paymentMethod, tipCents };
if (showSplits && tipCents > 0 && tipSplits.length > 0) {
patchBody.tipSplits = tipSplits.map((r) => ({
staffId: r.staffId,
staffName: r.staffName,
sharePct: r.pct,
}));
}
const res = await fetch(`/api/invoices/${invoice.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status: "paid", paymentMethod, tipCents }),
body: JSON.stringify(patchBody),
});
if (!res.ok) {
const err = (await res.json()) as { error?: string };
throw new Error(err.error ?? `HTTP ${res.status}`);
}
// Save tip splits if applicable and tip > 0
if (showSplits && tipCents > 0 && tipSplits.length > 0) {
const totalPct = tipSplits.reduce((s, r) => s + r.pct, 0);
if (Math.abs(totalPct - 100) < 0.01) {
const splitsRes = await fetch(`/api/invoices/${invoice.id}/tip-splits`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
splits: tipSplits.map((r) => ({
staffId: r.staffId,
staffName: r.staffName,
sharePct: r.pct,
})),
}),
});
if (!splitsRes.ok) console.warn("Tip split save failed (non-blocking)");
}
}
onUpdated();
} catch (e: unknown) {
setError(e instanceof Error ? e.message : "Failed to update");