WFDASH-WEBAPP/src/lib/server/delivery-notes-service.test.ts

802 lines
28 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeEach, vi } from "vitest";
/**
* Tests for the server-side delivery-notes service.
*
* Mocks PocketBase with an in-memory store. The service is the
* only thing under test — the route layer is verified separately
* by integration / E2E.
*/
// In-memory store keyed by collection name. We use this to
// simulate PB across `getOne`, `getList`, `getFullList`, `create`,
// `update` calls without standing up a real PB instance.
const { store, getOrCreate, where, pbMock, clearStore } = vi.hoisted(() => {
const store: Record<string, Map<string, Record<string, unknown>>> = {};
const getOrCreate = (collection: string) => {
if (!store[collection]) store[collection] = new Map();
return store[collection];
};
const where = (collection: string, filter: string) => {
const map = getOrCreate(collection);
const items = Array.from(map.values());
if (!filter || !filter.trim()) return items;
const clauses = filter.split("&&").map((c) => c.trim());
return items.filter((item) =>
clauses.every((clause) => {
// PB supports two operators we use in this service:
// = — exact match (with quoted RHS)
// ~ — contains (substring match)
// Both are joined with a quoted RHS. Handle both.
const eq = clause.match(/^(\w+)\s*=\s*"([^"]+)"$/);
if (eq) {
const [, field, raw] = eq;
return String(item[field] ?? "") === raw;
}
const contains = clause.match(/^(\w+)\s*~\s*"([^"]+)"$/);
if (contains) {
const [, field, raw] = contains;
return String(item[field] ?? "").includes(raw);
}
return true;
}),
);
};
const clearStore = () => {
for (const k of Object.keys(store)) delete store[k];
};
const pbMock = {
collection: (name: string) => ({
async getOne(id: string) {
const item = getOrCreate(name).get(id);
if (!item) {
const err = new Error("not found") as Error & { status: number };
err.status = 404;
throw err;
}
return item;
},
async getList(page = 1, perPage = 25, opts: { filter?: string; sort?: string } = {}) {
let items = where(name, opts.filter || "");
// Honour PB's `-field` / `field` sort syntax. The real
// service uses `sort: "-delivery_note_number"` to find
// the most-recent DN — without sorting here, the test
// mock returns insertion order and the counter never
// advances.
if (opts.sort) {
const desc = opts.sort.startsWith("-");
const field = desc ? opts.sort.slice(1) : opts.sort;
items = [...items].sort((a, b) => {
const av = String(a[field] ?? "");
const bv = String(b[field] ?? "");
if (av === bv) return 0;
return desc ? (av < bv ? 1 : -1) : av < bv ? -1 : 1;
});
}
const start = (page - 1) * perPage;
return {
items: items.slice(start, start + perPage),
totalItems: items.length,
page,
perPage,
};
},
async getFullList(opts: { filter?: string } = {}) {
return where(name, opts.filter || "");
},
async create(data: Record<string, unknown>) {
const id = `mock-${Math.random().toString(36).slice(2, 10)}`;
const item = { id, ...data };
getOrCreate(name).set(id, item);
return item;
},
async update(id: string, data: Record<string, unknown>) {
const item = getOrCreate(name).get(id);
if (!item) {
const err = new Error("not found") as Error & { status: number };
err.status = 404;
throw err;
}
Object.assign(item, data);
return item;
},
}),
};
return { store, getOrCreate, where, pbMock, clearStore };
});
vi.mock("@/lib/server/pocketbase-admin", () => ({
getAdminPocketBase: vi.fn().mockResolvedValue(pbMock),
}));
import {
createQuoteFromDeliveryNote,
setDeliveryNoteCustomerPo,
setDeliveryNoteOrder,
markDeliveryNoteInvoiced,
markDeliveryNoteCompletedByInvoice,
createAdHocDeliveryNote,
generateDeliveryNoteNumberServer,
getDeliveryNoteDetail,
} from "./delivery-notes-service";
/** Seed a delivery note into the mock store. Returns its id. */
function seedDeliveryNote(overrides: Partial<Record<string, unknown>> = {}): string {
const id = "dn-1";
getOrCreate("delivery_notes").set(id, {
id,
delivery_note_number: "DN26071401",
status: "draft",
customer: "customer-1",
company: "company-1",
order: "order-1",
notes: "leave at back door",
customer_po: "",
delivery_date: "",
created_by: "user-1",
...overrides,
});
return id;
}
/** Seed an order_item into the mock store, stamped with the given
* delivery_note_number. Returns its id. */
function seedOrderItem(
deliveryNoteNumber: string,
overrides: Partial<Record<string, unknown>> = {},
): string {
const id = `oi-${Math.random().toString(36).slice(2, 8)}`;
getOrCreate("order_items").set(id, {
id,
order: "order-1",
delivery_note_number: deliveryNoteNumber,
description: "10x M8 bolts",
quantity: 10,
cost_price: 100,
selling_price: 150,
supplier: "supplier-1",
sku: "M8-50",
pricing_source: "catalogue",
item_status: "delivery_note",
notes: "",
...overrides,
});
return id;
}
beforeEach(() => {
clearStore();
});
describe("createQuoteFromDeliveryNote", () => {
it("creates a quote with status=delivery_note + one quote_item per order_item, and flips the note to sent", async () => {
// Seed with NO notes — the service falls back to the default
// description containing the DN number, which is what we
// want to assert.
const dnId = seedDeliveryNote({ notes: "" });
seedOrderItem("DN26071401");
seedOrderItem("DN26071401", { description: "20x M10 nuts", quantity: 20 });
const result = await createQuoteFromDeliveryNote(dnId);
expect(result.id).toBeTruthy();
expect(result.quote_number).toMatch(/^Q\d{6}\d{3}$/);
// Quote header is persisted with the right shape.
const quotes = Array.from(getOrCreate("quotes").values());
expect(quotes).toHaveLength(1);
const q = quotes[0] as Record<string, unknown>;
expect(q.status).toBe("delivery_note");
expect(q.delivery_note).toBe(dnId);
expect(q.customer).toBe("customer-1");
expect(q.company).toBe("company-1");
expect(q.validity_days).toBe(30);
expect(q.description).toContain("DN26071401");
// v2.1 fix: the RFQ number is the delivery note number, so
// the operator sees the DN ref on the quote's RFQ line and
// knows instantly "this came from a delivery note, not
// from a customer email."
expect(q.rfq_number).toBe("DN26071401");
// One quote_item per order_item.
const quoteItems = Array.from(getOrCreate("quote_items").values());
expect(quoteItems).toHaveLength(2);
const byDescription = new Map(
quoteItems.map((qi) => [
(qi as Record<string, unknown>).description,
qi as Record<string, unknown>,
]),
);
expect(byDescription.get("10x M8 bolts")?.selling_price).toBe(150);
expect(byDescription.get("10x M8 bolts")?.pricing_source).toBe("catalogue");
expect(byDescription.get("20x M10 nuts")?.quantity).toBe(20);
// Delivery note status flipped to "sent".
const dn = getOrCreate("delivery_notes").get(dnId) as Record<string, unknown>;
expect(dn.status).toBe("sent");
});
it("stamps pricing_source=manual when the order_item has no selling_price", async () => {
const dnId = seedDeliveryNote();
seedOrderItem("DN26071401", { selling_price: null });
await createQuoteFromDeliveryNote(dnId);
const qi = Array.from(getOrCreate("quote_items").values())[0] as Record<string, unknown>;
// The service falls back to 0 — operator will fill in the
// real price on the quote detail page.
expect(qi.selling_price).toBe(0);
expect(qi.pricing_source).toBe("manual");
});
it("throws when the delivery note is not in draft status", async () => {
const dnId = seedDeliveryNote({ status: "sent" });
seedOrderItem("DN26071401");
await expect(createQuoteFromDeliveryNote(dnId)).rejects.toThrow(
/must be in "draft" status/,
);
});
it("throws when a delivery-note quote already exists for this note", async () => {
const dnId = seedDeliveryNote();
seedOrderItem("DN26071401");
// Pre-seed an existing delivery-note quote linked to this note.
getOrCreate("quotes").set("existing-quote", {
id: "existing-quote",
quote_number: "Q202607001",
status: "delivery_note",
delivery_note: dnId,
});
await expect(createQuoteFromDeliveryNote(dnId)).rejects.toThrow(
/already exists/,
);
});
it("throws when the delivery note has no order_items", async () => {
const dnId = seedDeliveryNote();
// No seedOrderItem call → the items lookup returns [].
await expect(createQuoteFromDeliveryNote(dnId)).rejects.toThrow(
/No order items found/,
);
});
});
describe("setDeliveryNoteCustomerPo", () => {
it("stamps the PO and flips the note from sent → po_received", async () => {
const dnId = seedDeliveryNote({ status: "sent" });
const updated = await setDeliveryNoteCustomerPo(dnId, "PO-2026-1234");
expect(updated.customer_po).toBe("PO-2026-1234");
expect(updated.status).toBe("po_received");
});
it("trims whitespace before saving", async () => {
const dnId = seedDeliveryNote({ status: "sent" });
const updated = await setDeliveryNoteCustomerPo(dnId, " PO-2026-1234 ");
expect(updated.customer_po).toBe("PO-2026-1234");
});
it("rejects empty PO", async () => {
const dnId = seedDeliveryNote({ status: "sent" });
await expect(setDeliveryNoteCustomerPo(dnId, "")).rejects.toThrow(
/cannot be empty/i,
);
await expect(setDeliveryNoteCustomerPo(dnId, " ")).rejects.toThrow(
/cannot be empty/i,
);
});
it("allows re-setting the PO on a po_received note (typo correction)", async () => {
const dnId = seedDeliveryNote({
status: "po_received",
customer_po: "PO-2026-OLD",
});
const updated = await setDeliveryNoteCustomerPo(dnId, "PO-2026-NEW");
expect(updated.customer_po).toBe("PO-2026-NEW");
// Status unchanged — we only stamp, don't transition again.
expect(updated.status).toBe("po_received");
});
it("rejects setting PO on a draft note (must send first)", async () => {
const dnId = seedDeliveryNote({ status: "draft" });
await expect(setDeliveryNoteCustomerPo(dnId, "PO-1")).rejects.toThrow(
/must be sent first/,
);
});
});
describe("markDeliveryNoteInvoiced", () => {
it("flips order_items to ready_invoice and the note to invoiced", async () => {
const dnId = seedDeliveryNote({ status: "po_received" });
seedOrderItem("DN26071401");
seedOrderItem("DN26071401");
const updated = await markDeliveryNoteInvoiced(dnId);
expect(updated.status).toBe("invoiced");
// Both order_items flipped to ready_invoice.
const items = Array.from(getOrCreate("order_items").values()) as Array<
Record<string, unknown>
>;
expect(items).toHaveLength(2);
for (const it of items) {
expect(it.item_status).toBe("ready_invoice");
}
});
it("rejects when the note is not in po_received", async () => {
const dnId = seedDeliveryNote({ status: "draft" });
await expect(markDeliveryNoteInvoiced(dnId)).rejects.toThrow(
/must be in "po_received" status/,
);
});
it("rejects when the note has no order_items", async () => {
const dnId = seedDeliveryNote({ status: "po_received" });
await expect(markDeliveryNoteInvoiced(dnId)).rejects.toThrow(
/No order items found/,
);
});
});
describe("markDeliveryNoteCompletedByInvoice", () => {
it("flips status to completed and appends a QuickBooks banner to notes", async () => {
const dnId = seedDeliveryNote({
status: "invoiced",
notes: "leave at back door",
});
const updated = await markDeliveryNoteCompletedByInvoice(dnId, {
invoiceNumber: "QB-INV-1234",
sentByName: "David Els",
sentAtIso: "2026-08-16T12:34:56.000Z",
});
expect(updated.status).toBe("completed");
// Existing notes preserved, banner appended on a new line.
expect(updated.notes).toBe(
"leave at back door\nAuto-closed via QuickBooks invoice QB-INV-1234 by David Els on 2026-08-16T12:34:56.000Z",
);
});
it("works when notes is empty (banner is the first content)", async () => {
const dnId = seedDeliveryNote({ status: "invoiced", notes: "" });
const updated = await markDeliveryNoteCompletedByInvoice(dnId, {
invoiceNumber: "QB-INV-9999",
sentByName: "Laura",
sentAtIso: "2026-08-16T08:00:00.000Z",
});
expect(updated.status).toBe("completed");
expect(updated.notes).toBe(
"Auto-closed via QuickBooks invoice QB-INV-9999 by Laura on 2026-08-16T08:00:00.000Z",
);
});
it("omits the 'by <name>' segment when sentByName is empty", async () => {
const dnId = seedDeliveryNote({ status: "invoiced", notes: "" });
const updated = await markDeliveryNoteCompletedByInvoice(dnId, {
invoiceNumber: "QB-INV-7",
sentByName: " ",
sentAtIso: "2026-08-16T08:00:00.000Z",
});
expect(updated.notes).toBe(
"Auto-closed via QuickBooks invoice QB-INV-7 on 2026-08-16T08:00:00.000Z",
);
});
it("rejects when the note is completed or cancelled (terminal statuses)", async () => {
const completedDn = seedDeliveryNote({ status: "completed" });
await expect(
markDeliveryNoteCompletedByInvoice(completedDn, {
invoiceNumber: "QB-1",
}),
).rejects.toThrow(/in "completed" status cannot be auto-closed/);
const cancelledDn = seedDeliveryNote({ status: "cancelled" });
await expect(
markDeliveryNoteCompletedByInvoice(cancelledDn, {
invoiceNumber: "QB-1",
}),
).rejects.toThrow(/in "cancelled" status cannot be auto-closed/);
});
it("accepts any non-terminal status and flips to completed", async () => {
// The QB "invoice + close" button is now shown on every active
// DN. Verify each non-terminal status is accepted. Each iteration
// re-seeds dn-1 with the target status (the seed helper always
// uses the same id), then runs the service.
const active = ["draft", "sent", "po_received", "invoiced"] as const;
for (const status of active) {
const dnId = seedDeliveryNote({
status,
notes: "",
});
const updated = await markDeliveryNoteCompletedByInvoice(dnId, {
invoiceNumber: `QB-${status}`,
sentByName: "David",
sentAtIso: "2026-08-16T12:00:00.000Z",
});
expect(updated.status).toBe("completed");
expect(updated.notes).toContain(`QB-${status}`);
}
});
it("rejects when invoiceNumber is empty", async () => {
const dnId = seedDeliveryNote({ status: "invoiced" });
await expect(
markDeliveryNoteCompletedByInvoice(dnId, { invoiceNumber: " " }),
).rejects.toThrow(/invoiceNumber is required/);
});
it("does not touch order_items (mirrors markDeliveryNoteComplete semantics)", async () => {
// The DN's items were already flipped to ready_invoice by
// markDeliveryNoteInvoiced, then n8n wrote them to "invoiced"
// when the QB invoice was created. Auto-close should not
// re-flip them — that's the same contract as the manual
// "Mark complete" button.
const dnId = seedDeliveryNote({ status: "invoiced" });
const itemId = seedOrderItem("DN26071401", { item_status: "invoiced" });
await markDeliveryNoteCompletedByInvoice(dnId, {
invoiceNumber: "QB-INV-1",
sentByName: "David",
sentAtIso: "2026-08-16T12:00:00.000Z",
});
const item = getOrCreate("order_items").get(itemId) as Record<string, unknown>;
expect(item.item_status).toBe("invoiced");
});
});
describe("generateDeliveryNoteNumberServer", () => {
it("generates a DN{YY}{MM}{DD}{NN} number for the current day", async () => {
const n = await generateDeliveryNoteNumberServer();
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const dd = String(now.getDate()).padStart(2, "0");
expect(n).toBe(`DN${yy}${mm}${dd}01`);
});
it("picks up from the highest existing DN number on order_items", async () => {
// Pre-seed an order_item with a high DN number. The service
// reads order_items.delivery_note_number (sorted -number) to
// find the latest, so the next call must return that + 1.
//
// (We don't test the "increments on each consecutive call"
// behavior here because the in-memory mock doesn't persist
// the generated number back to order_items — in real PB the
// delivery-note create flow writes the new number to every
// item before the next call sees it, but the mock can't
// simulate that without intercepting the generator. The
// production behavior is covered by the format test above
// + the create-flow tests in delivery-notes-service.)
const now = new Date();
const yy = String(now.getFullYear()).slice(-2);
const mm = String(now.getMonth() + 1).padStart(2, "0");
const dd = String(now.getDate()).padStart(2, "0");
const prefix = `DN${yy}${mm}${dd}`;
seedOrderItem("XXX-OLD", { delivery_note_number: `${prefix}05` });
const next = await generateDeliveryNoteNumberServer();
expect(next).toBe(`${prefix}06`);
});
});
describe("createAdHocDeliveryNote", () => {
it("creates a delivery_notes header + one order_item per line (no parent order)", async () => {
const result = await createAdHocDeliveryNote({
customer: "customer-1",
company: "company-1",
notes: "customer paid cash",
createdBy: "user-1",
items: [
{
description: "10x M8 bolts",
quantity: 10,
cost_price: 100,
},
{
description: "20x M10 nuts",
quantity: 20,
selling_price: 8,
},
],
});
expect(result.id).toBeTruthy();
expect(result.delivery_note_number).toMatch(/^DN\d{6}01$/);
// Header persisted with the right shape.
const dnotes = Array.from(getOrCreate("delivery_notes").values());
expect(dnotes).toHaveLength(1);
const dn = dnotes[0] as Record<string, unknown>;
expect(dn.delivery_note_number).toBe(result.delivery_note_number);
expect(dn.customer).toBe("customer-1");
expect(dn.company).toBe("company-1");
expect(dn.order).toBeNull();
expect(dn.status).toBe("draft");
expect(dn.notes).toBe("customer paid cash");
expect(dn.created_by).toBe("user-1");
// One order_item per input line, all with `order: null` and
// the delivery note number stamped on.
const items = Array.from(getOrCreate("order_items").values()) as Array<
Record<string, unknown>
>;
expect(items).toHaveLength(2);
const byDescription = new Map(
items.map((it) => [it.description, it]),
);
const bolts = byDescription.get("10x M8 bolts");
const nuts = byDescription.get("20x M10 nuts");
expect(bolts?.order).toBeNull();
expect(bolts?.delivery_note_number).toBe(result.delivery_note_number);
expect(bolts?.item_status).toBe("delivery_note");
expect(bolts?.quantity).toBe(10);
expect(bolts?.cost_price).toBe(100);
expect(nuts?.quantity).toBe(20);
expect(nuts?.selling_price).toBe(8);
});
it("rejects when customer is missing", async () => {
await expect(
createAdHocDeliveryNote({
customer: "",
items: [{ description: "x", quantity: 1 }],
}),
).rejects.toThrow(/Customer is required/);
});
it("rejects when items is empty", async () => {
await expect(
createAdHocDeliveryNote({
customer: "customer-1",
items: [],
}),
).rejects.toThrow(/At least one item/);
});
it("rejects items with empty description or non-positive quantity", async () => {
await expect(
createAdHocDeliveryNote({
customer: "customer-1",
items: [{ description: "", quantity: 1 }],
}),
).rejects.toThrow(/missing a description/);
await expect(
createAdHocDeliveryNote({
customer: "customer-1",
items: [{ description: "ok", quantity: 0 }],
}),
).rejects.toThrow(/invalid quantity/);
});
});
// ──────────────────────────────────────────────────────────────────────
// setDeliveryNoteOrder (Hermes unblock)
// ──────────────────────────────────────────────────────────────────────
describe("setDeliveryNoteOrder", () => {
it("links a delivery note to an order, returns {deliveryNote, previous}", async () => {
const dnId = seedDeliveryNote({ status: "draft", order: "" });
getOrCreate("orders").set("order-2", {
id: "order-2",
order_number: "ORD-2",
});
const result = await setDeliveryNoteOrder({
deliveryNoteId: dnId,
orderId: "order-2",
});
expect(result.previous).toEqual({ order: "" });
expect(result.deliveryNote.id).toBe(dnId);
expect(result.deliveryNote.order).toBe("order-2");
// The store reflects the write.
const stored = getOrCreate("delivery_notes").get(dnId);
expect(stored?.order).toBe("order-2");
});
it("refuses 409 delivery_note_locked when DN is invoiced", async () => {
const dnId = seedDeliveryNote({ status: "invoiced", order: "" });
getOrCreate("orders").set("order-2", { id: "order-2", order_number: "ORD-2" });
try {
await setDeliveryNoteOrder({ deliveryNoteId: dnId, orderId: "order-2" });
expect.fail("should have thrown");
} catch (err) {
expect((err as Error & { status?: number }).status).toBe(409);
const data = (err as Error & { data?: { code?: string; status?: string } }).data;
expect(data?.code).toBe("delivery_note_locked");
expect(data?.status).toBe("invoiced");
}
// Order must not have changed.
expect(getOrCreate("delivery_notes").get(dnId)?.order).toBe("");
});
it("refuses 409 delivery_note_locked when DN is completed", async () => {
const dnId = seedDeliveryNote({ status: "completed", order: "" });
getOrCreate("orders").set("order-2", { id: "order-2", order_number: "ORD-2" });
try {
await setDeliveryNoteOrder({ deliveryNoteId: dnId, orderId: "order-2" });
expect.fail("should have thrown");
} catch (err) {
expect((err as Error & { status?: number }).status).toBe(409);
const data = (err as Error & { data?: { code?: string; status?: string } }).data;
expect(data?.code).toBe("delivery_note_locked");
expect(data?.status).toBe("completed");
}
});
it("404 when the delivery note doesn't exist", async () => {
getOrCreate("orders").set("order-2", { id: "order-2", order_number: "ORD-2" });
await expect(
setDeliveryNoteOrder({ deliveryNoteId: "missing", orderId: "order-2" }),
).rejects.toMatchObject({ status: 404 });
});
it("404 when the order doesn't exist", async () => {
const dnId = seedDeliveryNote({ status: "draft", order: "" });
await expect(
setDeliveryNoteOrder({ deliveryNoteId: dnId, orderId: "missing" }),
).rejects.toMatchObject({ status: 404 });
});
it("is idempotent when the DN already points at the order (no-op + no audit stamp)", async () => {
const dnId = seedDeliveryNote({
status: "draft",
order: "order-1",
notes: "original dn note",
});
getOrCreate("orders").set("order-1", { id: "order-1", order_number: "ORD-1" });
const result = await setDeliveryNoteOrder({
deliveryNoteId: dnId,
orderId: "order-1",
actor: { id: "agent-1", name: "Hermes" },
});
expect(result.previous).toEqual({ order: "order-1" });
expect(result.deliveryNote.order).toBe("order-1");
// Notes must NOT be touched on idempotent no-op.
expect(getOrCreate("delivery_notes").get(dnId)?.notes).toBe("original dn note");
});
it("stamps actor into notes when provided", async () => {
const dnId = seedDeliveryNote({
status: "draft",
order: "",
notes: "existing dn note",
});
getOrCreate("orders").set("order-2", { id: "order-2", order_number: "ORD-2" });
await setDeliveryNoteOrder({
deliveryNoteId: dnId,
orderId: "order-2",
actor: { id: "agent-1", name: "Hermes" },
});
const stored = getOrCreate("delivery_notes").get(dnId);
expect(stored?.order).toBe("order-2");
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");
});
});
describe("getDeliveryNoteDetail", () => {
it("returns the note, its lines by number and the linked quote", async () => {
const dnId = seedDeliveryNote();
seedOrderItem("DN26071401");
seedOrderItem("DN26071401");
seedOrderItem("DN-OTHER");
getOrCreate("quotes").set("q-dn", { id: "q-dn", delivery_note: dnId, created: "2026-07-14" });
const detail = await getDeliveryNoteDetail(dnId);
expect(detail.deliveryNote.delivery_note_number).toBe("DN26071401");
expect(detail.items).toHaveLength(2);
expect(detail.quote?.id).toBe("q-dn");
});
it("404s for a missing note", async () => {
await expect(getDeliveryNoteDetail("missing")).rejects.toMatchObject({ status: 404 });
});
});