diff --git a/src/lib/server/order-delivery-note-link-service.test.ts b/src/lib/server/order-delivery-note-link-service.test.ts index 9cc874d..178a660 100644 --- a/src/lib/server/order-delivery-note-link-service.test.ts +++ b/src/lib/server/order-delivery-note-link-service.test.ts @@ -307,6 +307,65 @@ describe("suggestDeliveryNoteForOrder", () => { expect(result.best).toBeNull(); }); + + it("matches a delivery note at exactly 60 days old (boundary positive control)", async () => { + // 60 days before "now" 2026-08-30 = 2026-07-01 + const orderItem = { description: "M16x50 Bolt", quantity: 300 }; + const dnItem = { description: "M16x50 Bolt", quantity: 300, order: "other-order" }; + + mockAdminPb(makePb({ + [ORDERS]: { + getOne: vi.fn().mockResolvedValue({ id: "ord1", company: "co1" }), + update: vi.fn().mockResolvedValue({}), + }, + [ORDER_ITEMS]: { + getFullList: vi.fn() + .mockResolvedValueOnce([orderItem]) + .mockResolvedValueOnce([dnItem]), + }, + [DELIVERY_NOTES]: { + getFullList: vi.fn().mockResolvedValue([ + { + id: "dn-edge-in", + delivery_note_number: "DN-EDGE-IN", + company: "co1", + status: "completed", + created: "2026-07-01 12:00:00.000Z", // exactly 60 days back + order: "other-order", + }, + ]), + }, + })); + + const result = await suggestDeliveryNoteForOrder("ord1"); + + expect(result.best?.deliveryNoteId).toBe("dn-edge-in"); + }); + + it("does not match a delivery note just past the 60-day window (61 days old)", async () => { + // 61 days before "now" 2026-08-30 = 2026-06-30 + const orderItem = { description: "M16x50 Bolt", quantity: 300 }; + const dnItem = { description: "M16x50 Bolt", quantity: 300, order: "other-order" }; + + mockAdminPb(makePb({ + [ORDERS]: { + getOne: vi.fn().mockResolvedValue({ id: "ord1", company: "co1" }), + update: vi.fn().mockResolvedValue({}), + }, + [ORDER_ITEMS]: { + getFullList: vi.fn() + .mockResolvedValueOnce([orderItem]) + .mockResolvedValueOnce([dnItem]), + }, + [DELIVERY_NOTES]: { + getFullList: vi.fn().mockResolvedValue([]), // 61 days = filtered out by cutoff + }, + })); + + const result = await suggestDeliveryNoteForOrder("ord1"); + + expect(result.best).toBeNull(); + }); }); /* ============================================================================ diff --git a/src/lib/server/order-delivery-note-link-service.ts b/src/lib/server/order-delivery-note-link-service.ts index d858e20..a7979c0 100644 --- a/src/lib/server/order-delivery-note-link-service.ts +++ b/src/lib/server/order-delivery-note-link-service.ts @@ -34,11 +34,18 @@ const DELIVERY_NOTES_COLLECTION = "delivery_notes"; const QUOTES_COLLECTION = "quotes"; const QUOTE_ITEMS_COLLECTION = "quote_items"; +/** Maximum age (in days) of a delivery-note candidate considered for + * repeat matching. Beyond this window, even an exact-line match is + * treated as a coincidence, not a repeat order. */ +const WINDOW_DAYS = 60; + // Per 2026-08-30 business decision: match against a DN of ANY status, -// including `cancelled` — the prior exclusion missed real repeat orders -// where the original delivery's DN had been cancelled for reasons -// unrelated to whether goods went out. A 60-day candidate window -// (computed below) bounds the search space instead. +// including `cancelled` (previously excluded along with a hardcoded +// allowlist of "active" statuses — see git history) — the prior +// exclusion missed real repeat orders where the original delivery's DN +// had been cancelled for reasons unrelated to whether goods went out. +// A 60-day candidate window (computed below) bounds the search space +// instead. function esc(s: string): string { return (s || "").replace(/"/g, '\\"'); @@ -94,8 +101,8 @@ export async function suggestDeliveryNoteForOrder( quantity: Number(r.quantity) || 0, })); - const WINDOW_DAYS = 60; - const cutoff = new Date(Date.now() - WINDOW_DAYS * 24 * 60 * 60 * 1000); + const WINDOW_MS = WINDOW_DAYS * 24 * 60 * 60 * 1000; + const cutoff = new Date(Date.now() - WINDOW_MS); const cutoffStr = cutoff.toISOString().replace("T", " "); const candidates = await pb