From 05eaa1fa3e6ec935dd4941495340c495750e8c3f Mon Sep 17 00:00:00 2001 From: DEls Date: Sat, 19 Sep 2026 12:20:30 +0200 Subject: [PATCH] fix: preserve terminal staged candidates --- src/lib/dn-pipeline/run-staged.test.ts | 35 ++++++++++++++++++++++++++ src/lib/dn-pipeline/run-staged.ts | 6 +++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/lib/dn-pipeline/run-staged.test.ts b/src/lib/dn-pipeline/run-staged.test.ts index 0ee25b7..170bbc0 100644 --- a/src/lib/dn-pipeline/run-staged.test.ts +++ b/src/lib/dn-pipeline/run-staged.test.ts @@ -348,4 +348,39 @@ describe("runStagedBatch", () => { expect(fixture.candidates.get("old-b")).toEqual(expect.objectContaining({ pages: [9], delivery_note_number: "dn 1", stage: "needs_review" })); expect(fixture.candidates.size).toBe(3); }); + + it("does not rewrite an unmatched legacy promoted candidate", async () => { + const fixture = stagedPb(); + fixture.candidates.set("promoted", { + id: "promoted", batch: BATCH_ID, candidate_key: "promoted-key", note_index: 7, pages: [99], + delivery_note_number: "DN-HISTORIC", doc_number: "DN-HISTORIC", status: "promoted", + }); + normaliseSourceMock.mockResolvedValue([{ sourcePage: 1, png: Buffer.from("one"), mime: "image/png", width: 100, height: 200, rotation: 0 }]); + extractPageMock.mockResolvedValue(read(1, "DN-CURRENT", "PO-1")); + + await runStagedBatch(fixture.pb as never, BATCH_ID); + + expect(fixture.candidates.get("promoted")).toEqual(expect.objectContaining({ + status: "promoted", note_index: 7, + })); + expect(fixture.candidates.get("promoted")).not.toHaveProperty("stage"); + }); + + it("does not rewrite an unmatched candidate already related to a delivery note", async () => { + const fixture = stagedPb(); + fixture.candidates.set("posted-relation", { + id: "posted-relation", batch: BATCH_ID, candidate_key: "posted-relation-key", note_index: 8, pages: [98], + delivery_note_number: "DN-POSTED", doc_number: "DN-POSTED", delivery_note: "delivery-note-1", + }); + normaliseSourceMock.mockResolvedValue([{ sourcePage: 1, png: Buffer.from("one"), mime: "image/png", width: 100, height: 200, rotation: 0 }]); + extractPageMock.mockResolvedValue(read(1, "DN-CURRENT", "PO-1")); + + await runStagedBatch(fixture.pb as never, BATCH_ID); + + expect(fixture.candidates.get("posted-relation")).toEqual(expect.objectContaining({ + delivery_note: "delivery-note-1", note_index: 8, + })); + expect(fixture.candidates.get("posted-relation")).not.toHaveProperty("status"); + expect(fixture.candidates.get("posted-relation")).not.toHaveProperty("stage"); + }); }); diff --git a/src/lib/dn-pipeline/run-staged.ts b/src/lib/dn-pipeline/run-staged.ts index 7239b05..d0b81c6 100644 --- a/src/lib/dn-pipeline/run-staged.ts +++ b/src/lib/dn-pipeline/run-staged.ts @@ -34,6 +34,7 @@ type ExistingCandidate = { pages?: unknown; delivery_note_number?: string; doc_number?: string; + delivery_note?: string; status?: string; stage?: string; posted_at?: string; @@ -271,7 +272,7 @@ async function upsertCandidate( async function loadCandidates(pb: PocketBase, batchId: string): Promise { const records = (await pb.collection("dn_batch_notes").getFullList({ filter: batchFilter(pb, batchId), - fields: "id,candidate_key,note_index,pages,delivery_note_number,doc_number,status,stage,posted_at", + fields: "id,candidate_key,note_index,pages,delivery_note_number,doc_number,delivery_note,status,stage,posted_at", })) as unknown as ExistingCandidate[]; return records; } @@ -332,7 +333,8 @@ function legacyNumberCompatible(candidate: ExistingCandidate, documentNumber: st } function isTerminalCandidate(candidate: ExistingCandidate): boolean { - return candidate.status === "dismissed" || candidate.stage === "dismissed" || candidate.stage === "posted" || Boolean(candidate.posted_at); + return candidate.status === "dismissed" || candidate.status === "promoted" || candidate.status === "posted" || + candidate.stage === "dismissed" || candidate.stage === "posted" || Boolean(candidate.posted_at) || Boolean(candidate.delivery_note); } async function markSurplusCandidatesForReview(