From 9996923bf3edca362b2b271306ea7cc2be530efa Mon Sep 17 00:00:00 2001 From: Dave Date: Sun, 30 Aug 2026 18:28:44 +0200 Subject: [PATCH] fix(invoicing): guard the QuickBooks send against an all-excluded group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/(app)/invoicing/page.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/(app)/invoicing/page.tsx b/src/app/(app)/invoicing/page.tsx index 75e71b8..af07ce5 100644 --- a/src/app/(app)/invoicing/page.tsx +++ b/src/app/(app)/invoicing/page.tsx @@ -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) {