/** * Background processing for one inbox document: run the extraction pipeline on * the PDF bytes and persist the result (status → parsed) or the error * (status → error) via the REST store. Called fire-and-forget from upload (with * the in-memory buffer) and reparse (buffer re-fetched from Strapi). Never throws. * * The captured ctx carries the iDempiere token obtained at request time; OCR jobs * finish well within the token lifetime. */ import crypto from 'node:crypto' import { setInboxParsed, setInboxError, findInboxDocumentsBySize, type InboxCtx } from './inboxDb' import { runPipeline } from './extractPipeline' import { renderThumbDataUrl } from './rasterizePdf' export const processInboxDocument = async ( ctx: InboxCtx, id: number, pdf: Buffer, meta: { mimeType?: string; filename?: string } = {} ): Promise => { try { const res = await runPipeline(pdf, { ctx, mimeType: meta.mimeType, filename: meta.filename }) // page-1 thumbnail for the inbox cards (Strapi can't thumbnail PDFs); best-effort let thumb = '' if (!/xml/i.test(meta.mimeType || '')) { try { thumb = await renderThumbDataUrl(pdf) } catch {} } const extracted: any = thumb ? { ...(res.extracted || {}), __thumb: thumb } : { ...(res.extracted || {}) } // File signature + duplicate-upload guard: the same bytes (sha256; for rows stored // before the hash existed: same size + file name) already in the inbox → flag it so // the list/review can warn instead of offering a second booking. Non-blocking, fail-soft. const sha256 = crypto.createHash('sha256').update(pdf).digest('hex') extracted.__file = { sha256, size: pdf.length } try { const twins = await findInboxDocumentsBySize(ctx, pdf.length, id) const twin = twins.find((d: any) => d.extracted?.__file?.sha256 ? d.extracted.__file.sha256 === sha256 : (d.filename && d.filename === meta.filename)) if (twin) extracted.__duplicateOf = { id: twin.id, status: twin.status, cInvoiceId: twin.cInvoiceId || null, filename: twin.filename } } catch {} await setInboxParsed(ctx, id, { sourceTier: res.sourceTier, vendorFingerprint: res.vendorFingerprint, extracted, confidence: res.confidence, bbox: res.bbox }) } catch (e: any) { try { await setInboxError(ctx, id, e?.message || 'Processing failed') } catch {} } } export default processInboxDocument