import { readFile, writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; import { z } from 'zod'; import { VelrimClient, type ExtractResponseBody } from '@velrim/sdk'; // Decimal strings retain printed precision; normalization belongs in business checks. export const LineInvoice = z.object({ invoice_number: z.string(), currency: z.string(), line_items: z.array( z.object({ description: z.string(), quantity: z.string(), unit_price: z.string(), amount: z.string(), }), ), }); function cell(value: string): string { // Reject ambiguous spreadsheet input instead of silently changing source values. if (/^[=+\-@=+-@]/u.test(value.normalize('NFKC').trimStart()) || /^[\t\r\n]/u.test(value)) { throw new Error('Spreadsheet formula-like text: inspect the JSON evidence before export.'); } return `"${value.replaceAll('"', '""')}"`; } export function invoiceCsv(body: ExtractResponseBody) { const invoice = LineInvoice.parse(body.data); const reasons: string[] = []; if (!invoice.line_items.length) reasons.push('No line items returned; check the document.'); const pointers = ['/invoice_number', '/currency']; invoice.line_items.forEach((_row, i) => { for (const key of ['description', 'quantity', 'unit_price', 'amount']) { pointers.push(`/line_items/${i}/${key}`); } }); for (const pointer of pointers) { const field = body.fields[pointer]; if ( !field || field.state !== 'present' || field.conflict || field.grounding !== 'verified' || !field.anchor || typeof field.confidence !== 'number' || !Number.isFinite(field.confidence) || field.confidence < 0 || field.confidence > 1 ) reasons.push(pointer); } const rows = [ ['invoice_number', 'currency', 'line_index', 'description', 'quantity', 'unit_price', 'amount'], ...invoice.line_items.map((row, i) => [ invoice.invoice_number, invoice.currency, String(i), row.description, row.quantity, row.unit_price, row.amount, ]), ]; return { csv: rows.map((row) => row.map(cell).join(',')).join('\r\n') + '\r\n', reasons }; } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const [mode, input, prefix] = process.argv.slice(2); if (!input || !prefix || (mode !== 'json' && mode !== 'pdf')) { throw new Error( 'Usage: npx tsx line-items.ts json response.json output-prefix (or pdf invoice.pdf output-prefix)', ); } let body: ExtractResponseBody; if (mode === 'pdf') { // Raw schema retains partial responses before strict application validation. const result = await new VelrimClient().extract(z.toJSONSchema(LineInvoice), { bytes: await readFile(input), }); body = { data: result.data as Record, fields: result.fields, meta: result.meta, }; } else { body = JSON.parse(await readFile(input, 'utf8')) as ExtractResponseBody; } // Keep the envelope even if validation or spreadsheet checks reject the export. await writeFile(`${prefix}.evidence.json`, JSON.stringify(body, null, 2), { encoding: 'utf8', flag: 'wx', }); const { csv, reasons } = invoiceCsv(body); await writeFile(`${prefix}.csv`, csv, { encoding: 'utf8', flag: 'wx' }); console.log({ request_id: body.meta.request_id, evidence_checks_flagged: reasons.length, next: 'review_and_business_checks', }); }