diff --git a/src/lib/server/delivery-notes-service.test.ts b/src/lib/server/delivery-notes-service.test.ts index 2112c69..c5fa4ca 100644 --- a/src/lib/server/delivery-notes-service.test.ts +++ b/src/lib/server/delivery-notes-service.test.ts @@ -173,6 +173,7 @@ import { markDeliveryNoteInvoiced, markDeliveryNoteCompletedByInvoice, createAdHocDeliveryNote, + updateDeliveryNoteItemPricing, generateDeliveryNoteNumberServer, getDeliveryNoteDetail, } from "./delivery-notes-service"; @@ -658,6 +659,63 @@ describe("generateDeliveryNoteNumberServer", () => { }); }); +describe("qty_collected on counter delivery notes", () => { + // A counter note is a customer taking goods off the shelf. There is + // no driver pickup, so the collection has already happened by the + // time the note exists. `qty_collected` is what the invoice path + // reads to decide how many units to bill, and leaving it at 0 made + // every counter invoice a zero-quantity line, which QuickBooks + // rejects (n8n 400 -> an opaque 502 at the operator). + + it("stamps qty_collected = quantity on ad-hoc items at creation", async () => { + await createAdHocDeliveryNote({ + customer: "customer-1", + items: [ + { description: "Husqvarna blower", quantity: 11, cost_price: 100 }, + { description: "Slideway oil", quantity: 1, cost_price: 50 }, + ], + }); + const items = [...getOrCreate("order_items").values()] as Array< + Record + >; + expect(items).toHaveLength(2); + for (const item of items) { + expect(item.qty_collected).toBe(item.quantity); + } + }); + + it("follows the quantity when a COUNTER note is edited", async () => { + // No parent order -> the note quantity IS the collected quantity. + seedDeliveryNote({ order: "" }); + const oiId = seedOrderItem("DN26071401", { order: null, quantity: 10 }); + await updateDeliveryNoteItemPricing({ itemId: oiId, quantity: 5 }); + const item = getOrCreate("order_items").get(oiId) as Record; + expect(item.quantity).toBe(5); + expect(item.qty_collected).toBe(5); + }); + + it("leaves an ORDER-linked note's collected qty alone", async () => { + // Order-linked notes are owned by the driver pickup flow, where + // qty_collected means "in the van". Overwriting it with the order + // qty would bill for goods that have not arrived. + seedDeliveryNote({ order: "order-1" }); + const oiId = seedOrderItem("DN26071401", { order: "order-1", quantity: 10 }); + await updateDeliveryNoteItemPricing({ itemId: oiId, quantity: 5 }); + const item = getOrCreate("order_items").get(oiId) as Record; + expect(item.quantity).toBe(5); + expect(item.qty_collected).toBeUndefined(); + }); + + it("does not touch collected when only the price changes", async () => { + seedDeliveryNote({ order: "" }); + const oiId = seedOrderItem("DN26071401", { order: null, quantity: 10 }); + await updateDeliveryNoteItemPricing({ itemId: oiId, cost_price: 250 }); + const item = getOrCreate("order_items").get(oiId) as Record; + expect(item.cost_price).toBe(250); + expect(item.qty_collected).toBeUndefined(); + }); +}); + describe("createAdHocDeliveryNote", () => { it("creates a delivery_notes header + one order_item per line (no parent order)", async () => { const result = await createAdHocDeliveryNote({ diff --git a/src/lib/server/delivery-notes-service.ts b/src/lib/server/delivery-notes-service.ts index 8d360e2..b5e03ea 100644 --- a/src/lib/server/delivery-notes-service.ts +++ b/src/lib/server/delivery-notes-service.ts @@ -1277,6 +1277,18 @@ export async function createAdHocDeliveryNote( delivery_note_number: deliveryNoteNumber, description: it.description.trim(), quantity: Number(it.quantity), + // Ad-hoc = counter sale. The customer is standing at the + // counter taking the goods, so the collection has ALREADY + // happened by the time this row exists — there is no driver + // pickup to record. `qty_collected` is what the invoice path + // reads to decide how many units to bill, and leaving it 0 + // produced a zero-quantity invoice line, which QuickBooks + // rejects (n8n 400 -> an opaque 502 at the operator). + // + // "It can even be collected before we even had the system" — + // which is exactly the case: nothing here is ever picked up by + // a driver, so the note quantity IS the collected quantity. + qty_collected: Number(it.quantity), supplier: it.supplier ?? null, cost_price: Number(it.cost_price ?? 0), selling_price: @@ -2200,6 +2212,21 @@ export async function updateDeliveryNoteItemPricing( } if (hasQty) patch.quantity = Number(input.quantity); + // A COUNTER delivery note has no parent order, which means the + // goods left the shelf and were handed over — the collection + // already happened. `qty_collected` is the field the invoice path + // reads to decide how many units to bill, so leaving it at 0 makes + // the invoice come out as a zero-quantity line, which QuickBooks + // rejects outright (n8n 400, surfaced as an opaque 502). + // + // Only for parentless notes: an order-linked note is owned by the + // driver pickup flow, where `qty_collected` means "the driver has + // this many in the van" and overwriting it with the order qty would + // bill the customer for goods that have not arrived. + if (hasQty && !dn.order) { + patch.qty_collected = Number(input.quantity); + } + // Stamp the audit tag on every pricing change. Operator- // typed values are tagged "manual" so downstream surfaces // (process page, sprint rows) can flag it for re-review if