fix(invoicing): guard the QuickBooks send against an all-excluded group

Review follow-up to the non-invoiceable-extras work. Task 6 of the plan
called for the billable-subset guard on `sendToQuickBooks` as well as
in the modals; it landed in the modals only.

`rows.length === 0` counts excluded rows, which ride along for the n8n
status flip but never bill. A group where every row is excluded passes
that check and ships an empty `items` array, so QuickBooks would create
a zero-line invoice while n8n still flipped every row to `invoiced`.

Not reachable through the current UI — both live modals disable submit
on this case. This is defence-in-depth for the callers that bypass
them: the (currently unrendered) bulk modal and any legacy call passing
`overrides: undefined`, which falls back to every row in the group.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-30 18:28:44 +02:00
parent 7a1239a187
commit 9996923bf3

View file

@ -23,6 +23,7 @@ import {
buildDeliveryNotePrintGroups,
buildTodayUnprintedDeliveryNoteGroups,
buildQbInvoicePayload,
billableRows,
nameSimilarity,
buildOrderInvoiceCards,
buildRecentInvoiceRange,
@ -2048,6 +2049,20 @@ export default function InvoicingPage() {
toast.error("Pick at least one item before sending to QuickBooks.");
return false;
}
// Task 6 of the non-invoiceable-order-extras plan. `rows.length`
// above counts excluded rows, which ride along for the n8n
// status flip but never bill. If every row in the group is
// excluded the payload's `items` array would be empty and
// QuickBooks would receive a zero-line invoice.
//
// Both live modals disable submit on this case, so this is
// defence-in-depth for the callers that bypass them: the
// (currently unrendered) bulk modal and any legacy call that
// passes `overrides: undefined` and falls back to every row.
if (billableRows(rows).length === 0) {
toast.error("This order has no billable lines.");
return false;
}
const first = rows[0];
const similarity = nameSimilarity(customer.name, first.companyName ?? "");
if (similarity < 0.35) {