diff --git a/src/app/api/invoicing/delivery-note-print/route.test.ts b/src/app/api/invoicing/delivery-note-print/route.test.ts index ba362d4..1f23f40 100644 --- a/src/app/api/invoicing/delivery-note-print/route.test.ts +++ b/src/app/api/invoicing/delivery-note-print/route.test.ts @@ -53,6 +53,29 @@ describe("GET /api/invoicing/delivery-note-print", () => { }), ); }); + + // Task 8 of the non-invoiceable-order-extras plan. The DN print + // route queries PB by delivery_note_number + item_status. Excluded + // rows DO receive that DN number + status (Task 7 / T7's status + // flip loop is left untouched), so they are picked up + // automatically. The picker must see the washers — the same + // regression as print-bundle: lock the no-filter behaviour in. + it("does not filter excluded rows out of the delivery-note query", async () => { + getFullListMock.mockResolvedValueOnce([]); + await GET( + makeRequest( + "https://test.local/api/invoicing/delivery-note-print?deliveryNoteNumber=DN26071401", + ) as any, + ); + const filter = (getFullListMock.mock.calls[0]?.[0] as { filter: string }).filter; + // The filter MUST include the DN number and the status, and + // MUST NOT carry an exclude_from_invoice clause. Adding one + // would silently filter the picker slip — and break Task 7's + // design (excluded rows ride along for the status flip). + expect(filter).toContain("delivery_note_number = \"DN26071401\""); + expect(filter).toContain("item_status = \"delivery_note\""); + expect(filter).not.toContain("exclude_from_invoice"); + }); }); describe("POST /api/invoicing/delivery-note-print", () => { diff --git a/src/app/api/invoicing/print-bundle/route.test.ts b/src/app/api/invoicing/print-bundle/route.test.ts index ccc6406..bd4ea23 100644 --- a/src/app/api/invoicing/print-bundle/route.test.ts +++ b/src/app/api/invoicing/print-bundle/route.test.ts @@ -113,6 +113,52 @@ describe("GET /api/invoicing/print-bundle", () => { expect(fetchMock).not.toHaveBeenCalled(); }); + it("picks only the customer quantity and sends the MOQ overflow to stock", async () => { + // Customer ordered 4; supplier MOQ forced 10 onto the PO and all 10 + // arrived. The picker must send 4 to the customer and put 6 into + // stock — the "ordered" column is the CUSTOMER's number, not the + // supplier's. Reading order_qty here told the picker to send all 10 + // and reported no surplus at all. + getFullList.mockResolvedValueOnce([ + { + id: "item-1", + description: "Washer", + qb_invoice_pdf: "inv.pdf", + quantity: 4, + order_qty: 10, + qty_collected: 10, + order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } }, + expand: { + order: { + id: "ord-1", + order_number: "ORD-2026-07-001", + expand: { company: { company: "CNL" }, customer: { customer: "Acme" } }, + }, + purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } }, + }, + }, + ]); + + fetchMock.mockResolvedValueOnce(new Response(new Uint8Array([9, 9]), { status: 200 })); + buildPrintBundle.mockResolvedValueOnce(new Uint8Array([1, 2, 3])); + + const res = await GET(makeGetRequest("INV-1") as any); + expect(res.status).toBe(200); + + const props = (pickingSlipPdfMock.mock.calls[0]?.[0] ?? {}) as unknown as { + lines: Array<{ + line: { ordered: number; collected: number; pickQty: number; outstanding: number; toStock: number }; + }>; + }; + expect(props.lines[0].line).toMatchObject({ + ordered: 4, + collected: 10, + pickQty: 4, + outstanding: 0, + toStock: 6, + }); + }); + it("returns a bundled PDF on success", async () => { getFullList.mockResolvedValueOnce([ { @@ -224,4 +270,70 @@ describe("GET /api/invoicing/print-bundle", () => { }), ); }); + + // Task 8 of the non-invoiceable-order-extras plan. The + // non-invoiceable feature relies on a property of the existing + // print pipeline: excluded rows DO receive item_status = + // "invoiced" (Task 7 / n8n batch flip) and DO carry the invoice + // number, so the print-bundle query picks them up automatically. + // The picker must still pull the washers even though they never + // reach the customer. This test locks that behaviour in. + it("includes excluded rows in the picking slip (picker pulls the washers)", async () => { + getFullList.mockResolvedValueOnce([ + { + id: "item-bill", + description: "Bolt", + qb_invoice_pdf: "inv.pdf", + order_qty: 4, + qty_collected: 4, + order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } }, + expand: { + order: { + id: "ord-1", + order_number: "ORD-2026-07-001", + expand: { company: { company: "CNL" }, customer: { customer: "Acme" } }, + }, + purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } }, + }, + }, + { + id: "item-excl", + description: "Washer (supplier-side component)", + qb_invoice_pdf: "inv.pdf", + // Excluded from the customer invoice (T5) but still flipped + // to invoiced alongside its siblings (T7). The print query + // keys on item_status = "invoiced" + invoice.number, neither + // of which differs from a billable row — so this row lands + // in the picking slip via the existing query. + order_qty: 4, + qty_collected: 4, + order_item_details: { invoice: { number: "INV-1", date: "2026-07-09" } }, + exclude_from_invoice: true, + expand: { + order: { + id: "ord-1", + order_number: "ORD-2026-07-001", + expand: { company: { company: "CNL" }, customer: { customer: "Acme" } }, + }, + purchase_order: { expand: { supplier: { suppliername: "Alpha Bearings" } } }, + }, + }, + ]); + + fetchMock.mockResolvedValueOnce(new Response(new Uint8Array([9, 9]), { status: 200 })); + buildPrintBundle.mockResolvedValueOnce(new Uint8Array([1, 2, 3])); + + const res = await GET(makeGetRequest("INV-1") as any); + expect(res.status).toBe(200); + + // Both rows reached the picking slip renderer — the picker + // sees the washers, not just the bolt. + const props = (pickingSlipPdfMock.mock.calls[0]?.[0] ?? {}) as unknown as { + lines: Array<{ line: { description?: string } }>; + }; + expect(props.lines).toHaveLength(2); + const descriptions = props.lines.map((l) => l.line.description); + expect(descriptions).toContain("Bolt"); + expect(descriptions).toContain("Washer (supplier-side component)"); + }); });