test(invoicing): lock in picking-slip inclusion of excluded rows

Task 8 of the non-invoiceable-order-extras plan.

Two regression tests, no production code change. The picking-slip
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 / DN number, so the
print queries pick them up automatically. The picker must still
pull the washers even though they never reach the customer.

- print-bundle: explicitly verifies the picking slip renderer is
  called with both billable and excluded rows (mocked
  PickingSlipPdf → assert lines array has both descriptions).
- delivery-note-print: explicitly verifies the PB filter does NOT
  carry an exclude_from_invoice clause — adding one would
  silently filter the picker slip and break Task 7's design.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dave 2026-08-30 17:44:12 +02:00
parent cb3f1618c4
commit f7a79e671a
2 changed files with 135 additions and 0 deletions

View file

@ -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", () => {

View file

@ -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)");
});
});