diff --git a/src/lib/server/delivery-notes-service.test.ts b/src/lib/server/delivery-notes-service.test.ts index db97218..6af3676 100644 --- a/src/lib/server/delivery-notes-service.test.ts +++ b/src/lib/server/delivery-notes-service.test.ts @@ -705,4 +705,75 @@ describe("setDeliveryNoteOrder", () => { expect(String(stored?.notes ?? "")).toMatch(/existing dn note/); expect(String(stored?.notes ?? "")).toMatch(/Hermes linked this delivery note to order order-2/); }); + + // Regression coverage for the delivery-notes backfill (2026-09-07): + // DNs created while the /api/invoicing/delivery-note `fields`+`expand` + // bug was live (see route.ts) got saved with customer/company/order + // all blank. Re-linking the order via this setter should also + // backfill customer/company from that order, since they're supposed + // to mirror it — see the docstring above `setDeliveryNoteOrder`. + it("backfills customer/company from the order when the DN's own values are blank", async () => { + const dnId = seedDeliveryNote({ status: "draft", order: "", customer: "", company: "" }); + getOrCreate("orders").set("order-2", { + id: "order-2", + order_number: "ORD-2", + customer: "customer-9", + company: "company-9", + }); + + const result = await setDeliveryNoteOrder({ + deliveryNoteId: dnId, + orderId: "order-2", + }); + + expect(result.deliveryNote.customer).toBe("customer-9"); + expect(result.deliveryNote.company).toBe("company-9"); + const stored = getOrCreate("delivery_notes").get(dnId); + expect(stored?.customer).toBe("customer-9"); + expect(stored?.company).toBe("company-9"); + }); + + it("backfills customer/company even when the order is already linked (idempotent-on-order path)", async () => { + // A DN can already point at the right order (e.g. re-running this + // setter a second time) yet still be missing customer/company — + // exactly the shape the fields+expand regression left behind. + // The idempotent-on-order branch must still apply the backfill. + const dnId = seedDeliveryNote({ status: "draft", order: "order-2", customer: "", company: "" }); + getOrCreate("orders").set("order-2", { + id: "order-2", + order_number: "ORD-2", + customer: "customer-9", + company: "company-9", + }); + + const result = await setDeliveryNoteOrder({ + deliveryNoteId: dnId, + orderId: "order-2", + }); + + expect(result.previous).toEqual({ order: "order-2" }); + expect(result.deliveryNote.customer).toBe("customer-9"); + expect(result.deliveryNote.company).toBe("company-9"); + }); + + it("does not overwrite a customer/company the DN already has", async () => { + const dnId = seedDeliveryNote({ + status: "draft", + order: "", + customer: "customer-1", + company: "company-1", + }); + getOrCreate("orders").set("order-2", { + id: "order-2", + order_number: "ORD-2", + customer: "customer-9", + company: "company-9", + }); + + await setDeliveryNoteOrder({ deliveryNoteId: dnId, orderId: "order-2" }); + + const stored = getOrCreate("delivery_notes").get(dnId); + expect(stored?.customer).toBe("customer-1"); + expect(stored?.company).toBe("company-1"); + }); }); diff --git a/src/lib/server/delivery-notes-service.ts b/src/lib/server/delivery-notes-service.ts index d243de1..ab0942c 100644 --- a/src/lib/server/delivery-notes-service.ts +++ b/src/lib/server/delivery-notes-service.ts @@ -512,6 +512,19 @@ export interface SetDeliveryNoteOrderInput { * WFDASH UI surfaces which changes came from the agent. The * `notes` field on `delivery_notes` is free-text (not a JSON * feed like `order_items.notes`). + * + * Customer/company backfill: `delivery_notes.customer` / + * `.company` are meant to mirror the linked order's customer / + * company (see the header-create block in the legacy + * `/api/invoicing/delivery-note` route). A DN created while those + * relations failed to resolve (e.g. the 2026-08-30 → 2026-09-07 + * `fields`-strips-`expand` regression — see git history on that + * route) ends up with `customer`/`company`/`order` all blank. When + * this setter links the order, it also fills `customer`/`company` + * from that order IF the DN's own values are currently blank — + * never overwrites an operator-set value. This makes re-linking + * the order via this one call enough to fully backfill an affected + * DN, instead of needing a second, nonexistent setter. */ export async function setDeliveryNoteOrder( input: SetDeliveryNoteOrderInput, @@ -543,9 +556,14 @@ export async function setDeliveryNoteOrder( } // 404 if the order doesn't exist. PB would 400 on a bad FK id, - // but a 404 from getOne is a cleaner signal for the caller. + // but a 404 from getOne is a cleaner signal for the caller. The + // record is kept (not discarded) — its `customer`/`company` + // relation ids feed the backfill below. + let order: { customer?: string; company?: string } | null = null; try { - await pb.collection(ORDERS).getOne(orderId); + order = await pb + .collection(ORDERS) + .getOne<{ customer?: string; company?: string }>(orderId); } catch { const err = new Error(`Order ${orderId} not found`); (err as Error & { status?: number }).status = 404; @@ -574,9 +592,29 @@ export async function setDeliveryNoteOrder( const previous: { order: string } = { order: dn.order || "" }; - // Idempotent no-op when the DN already points at this order. - // Don't write, don't stamp — just return the DN as-is. + // Backfill customer/company from the order — only when the DN's + // own value is currently blank. See the docstring above. Computed + // up front so it also applies on the idempotent (order-unchanged) + // path below: a DN can already point at the right order yet still + // be missing customer/company (exactly the shape the 2026-08-30 → + // 2026-09-07 `fields`-strips-`expand` bug left behind), and a + // second call with the same order id is the natural way an + // operator/agent re-runs this setter to fix that up. + const backfill: Record = {}; + if (!dn.customer && order?.customer) { + backfill.customer = order.customer; + } + if (!dn.company && order?.company) { + backfill.company = order.company; + } + + // Idempotent on `order`: don't re-write it, don't re-stamp notes — + // but still apply the customer/company backfill if there's one to + // apply, since that's a pure data-quality fix, not a "change". if (previous.order === orderId) { + if (Object.keys(backfill).length > 0) { + await pb.collection(DELIVERY_NOTES).update(deliveryNoteId, backfill); + } const fresh = await pb .collection(DELIVERY_NOTES) .getOne(deliveryNoteId); @@ -586,7 +624,7 @@ export async function setDeliveryNoteOrder( // Optional actor stamp: append a single-line audit entry onto // the DN's free-text notes field. We always also update the // `order` relation; the stamp is just an audit breadcrumb. - const update: Record = { order: orderId }; + const update: Record = { order: orderId, ...backfill }; if (input.actor?.id && input.actor?.name) { const stamp = `[${new Date().toISOString()}] ${input.actor.name} linked this delivery note to order ${orderId}`; const existingNotes = (dn.notes ?? "").toString();