fix(delivery-notes): backfill customer/company when linking a DN's order
setDeliveryNoteOrder only ever wrote the `order` relation. Delivery notes created while the fields+expand bug (see prior commit) was live got saved with customer/company/order all blank — this setter is the only existing entry point that touches the order relation, so it now also fills customer/company from the linked order when the DN's own values are blank (never overwrites an operator-set value). Applies on both the write path and the idempotent-order-unchanged path, so re-running the same call is enough to finish backfilling an affected DN once the order is already linked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6dc7204477
commit
20304442a7
2 changed files with 114 additions and 5 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {};
|
||||
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<DeliveryNote>(deliveryNoteId, backfill);
|
||||
}
|
||||
const fresh = await pb
|
||||
.collection(DELIVERY_NOTES)
|
||||
.getOne<DeliveryNote>(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<string, unknown> = { order: orderId };
|
||||
const update: Record<string, unknown> = { 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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue