← back to AbramsOS
routes/documents.js
97 lines
// Document operations — currently: parse a PDF and (optionally) create a purchase from it.
const express = require('express');
const fs = require('fs');
const path = require('path');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const pdf = require('../lib/pdf-parser');
const extractor = require('../lib/receipt-extractor');
const router = express.Router();
const DEV_USER_ID = 'user_steve';
router.get('/api/documents', async (_req, res) => {
const r = await db.query(
`SELECT id, kind, mime, object_path, parsed_status, created_at
FROM document WHERE user_id = $1 ORDER BY created_at DESC LIMIT 200`,
[DEV_USER_ID]
);
res.json(r.rows);
});
router.post('/api/documents/:id/parse', async (req, res) => {
const docId = req.params.id;
const r = await db.query(`SELECT * FROM document WHERE id = $1 AND user_id = $2`, [docId, DEV_USER_ID]);
if (!r.rows.length) return res.status(404).json({ error: 'document not found' });
const doc = r.rows[0];
if (doc.mime !== 'application/pdf') {
return res.status(400).json({ error: 'only application/pdf supported in tick 4 (image OCR is a later tick)' });
}
const filePath = pdf.resolveDocumentPath(doc.object_path, doc.mime);
if (!filePath || !fs.existsSync(filePath)) {
return res.status(404).json({ error: `file not found on disk: ${filePath}` });
}
try {
const parsed = await pdf.parseFile(filePath);
await db.query(`UPDATE document SET parsed_status = 'parsed' WHERE id = $1`, [docId]);
// Build a synthetic gmail-summary so the existing extractor can run unchanged.
const synthSummary = {
headers: { subject: path.basename(filePath), from: '', date: new Date(doc.created_at).toISOString() },
body: { text: parsed.text, html: '' },
};
let extracted = extractor.extract(synthSummary);
if (extracted && extracted.confidence < extractor.LLM_THRESHOLD) {
try {
extracted = await extractor.extractWithFallback(synthSummary, { llm: true, timeoutMs: 25_000 });
} catch (_) { /* keep heuristic */ }
}
let purchaseId = null;
if (extracted) {
purchaseId = id('purchase');
await db.query(
`INSERT INTO purchase
(id, user_id, source_message_id, merchant_name, merchant_domain, order_number, purchase_date, total_amount, currency, confidence, raw_extract)
VALUES ($1, $2, NULL, $3, $4, $5, $6, $7, $8, $9, $10)`,
[purchaseId, DEV_USER_ID, extracted.merchant, extracted.merchantDomain, extracted.orderNumber, extracted.purchaseDate, extracted.total, extracted.currency, extracted.confidence, extracted]
);
await audit.log({
actorType: 'system',
objectType: 'purchase',
objectId: purchaseId,
eventType: 'purchase_extracted',
metadata: { source: 'pdf', document_id: docId, merchant: extracted.merchant, total: extracted.total, confidence: extracted.confidence, source_tier: extracted.source },
});
}
await audit.log({
actorType: 'user',
actorId: DEV_USER_ID,
objectType: 'document',
objectId: docId,
eventType: 'document_parsed',
metadata: { pages: parsed.pages, chars: parsed.text.length, purchase_id: purchaseId },
});
res.json({
ok: true,
pages: parsed.pages,
chars: parsed.text.length,
preview: parsed.text.slice(0, 400),
purchase: extracted ? { id: purchaseId, ...extracted } : null,
});
} catch (err) {
console.error('[document parse]', err);
await db.query(`UPDATE document SET parsed_status = 'failed' WHERE id = $1`, [docId]);
res.status(500).json({ error: err.message });
}
});
module.exports = router;