diff --git a/src/lib/dn-pipeline/assemble.test.ts b/src/lib/dn-pipeline/assemble.test.ts index f7986d9..6f81bee 100644 --- a/src/lib/dn-pipeline/assemble.test.ts +++ b/src/lib/dn-pipeline/assemble.test.ts @@ -51,6 +51,53 @@ describe("assemblePages", () => { }); }); +describe("background fragments", () => { + const fragment = (over: Partial = {}) => page(1, { + multiple_documents: true, + items: [{ description: "Bend 90", qty: 7, unit: "ea", unit_price: null, line_amount: null, product_code: null }], + evidence: { multiple_documents: "A cropped corner of another paper is visible behind this invoice." }, + ...over, + }); + + it("groups a complete document that only has background paper behind it", () => { + const result = assemblePages([fragment()]); + expect(result.groups.map((group) => group.pages)).toEqual([[1]]); + expect(result.openPages).toEqual([]); + expect(result.requiresReview).toBe(false); + }); + + it("still holds a page showing two complete documents side by side", () => { + const result = assemblePages([fragment({ + evidence: { multiple_documents: "Two delivery notes side by side, each with its own header." }, + })]); + expect(result.groups).toEqual([]); + expect(result.openPages).toEqual([{ page: 1, reason: "multiple_documents" }]); + expect(result.requiresReview).toBe(true); + }); + + it("still holds a background fragment on a page with no line items", () => { + const result = assemblePages([fragment({ items: [] })]); + expect(result.openPages).toEqual([{ page: 1, reason: "multiple_documents" }]); + }); + + it("still holds a background fragment on a page with no document number", () => { + const result = assemblePages([fragment({ doc_number: "" })]); + expect(result.openPages).toEqual([{ page: 1, reason: "multiple_documents" }]); + }); + + it("still holds the page when the evidence names a second document number", () => { + const result = assemblePages([fragment({ + evidence: { multiple_documents: "Corner of invoice 22433700 visible behind, a second document." }, + })]); + expect(result.openPages).toEqual([{ page: 1, reason: "multiple_documents" }]); + }); + + it("groups a fragment with no evidence text at all on a complete page", () => { + const result = assemblePages([fragment({ evidence: {} })]); + expect(result.openPages).toEqual([{ page: 1, reason: "multiple_documents" }]); + }); +}); + it("preserves manual split/merge decisions instead of rejoining by printed numbers",()=>{ const reads=[page(1,{doc_number:"DN"}),page(2,{doc_number:"DN"}),page(3,{doc_number:"OTHER"})]; diff --git a/src/lib/dn-pipeline/assemble.ts b/src/lib/dn-pipeline/assemble.ts index 87f7814..5967c2d 100644 --- a/src/lib/dn-pipeline/assemble.ts +++ b/src/lib/dn-pipeline/assemble.ts @@ -43,8 +43,10 @@ export function assemblePages( const readsByPage = new Map(ordered.map((read) => [read.page, read])); for (const read of ordered) { if (read.multiple_documents) { - openPages.push({ page: read.page, reason: "multiple_documents" }); - continue; + if (!backgroundFragment(read)) { + openPages.push({ page: read.page, reason: "multiple_documents" }); + continue; + } } const previous = groups[groups.length - 1]; @@ -113,6 +115,28 @@ export function assemblePages( return { groups, openPages, requiresReview: openPages.length > 0 }; } +const BACKGROUND = /background|behind|fragment|cropped|partial|edge|corner|stack/i; +const DOCUMENT_NUMBER = /\d{4,}/g; +/** + * True when a page flagged `multiple_documents` is really one complete + * document with a scrap of other paper behind it. + * + * The extraction prompt already tells the reader to ignore fragments, and it + * still flags them often enough to hold whole invoices in "Check grouping" + * for the want of a corner. Every condition has to hold: a number of its own + * and at least one line, so there is a document to group, and evidence that + * describes the other paper as background without naming a second reference — + * a second reference means there is a second document to decide about. + */ +function backgroundFragment(read: PageRead): boolean { + if (!read.doc_number.trim() || read.items.length === 0) return false; + const evidence = read.evidence?.multiple_documents; + if (!evidence || !BACKGROUND.test(evidence)) return false; + const own = [read.doc_number, ...(read.doc_number_alts ?? [])].join("|"); + const elsewhere = evidence.replace(own, " ").match(DOCUMENT_NUMBER); + return elsewhere === null; +} + function sessionFor(page: number, hints: AssemblyHints): string | undefined { return hints.page_delivery_session_ids?.[page] ?? hints.delivery_session_ids?.[page] ?? (hints.delivery_session_id ?? hints.deliverySessionId);