/** * routes/export.mjs — Export endpoints (receipts, uploads) */ import { execFile } from 'child_process'; import { promisify } from 'util'; import { createWriteStream, unlinkSync } from 'fs'; const execFileAsync = promisify(execFile); const RECEIPT_BUCKET = 'landvex-prod'; const RECEIPT_REGION = 'eu-north-1'; async function s3Put(localPath, s3Key) { await execFileAsync('aws', [ 's3', 'cp', localPath, `s3://${RECEIPT_BUCKET}/${s3Key}`, '--region', RECEIPT_REGION, ], { env: { ...process.env, AWS_DEFAULT_REGION: RECEIPT_REGION } }); return `https://${RECEIPT_BUCKET}.s3.${RECEIPT_REGION}.amazonaws.com/${s3Key}`; } export default function registerExport(app, pool) { // POST /api/ledger/journal/:id/upload — raw file upload app.post('/api/ledger/journal/:id/upload', async (req, res) => { const { id } = req.params; const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group'; const filename = (req.headers['x-filename'] || 'receipt').replace(/[^a-zA-Z0-9._-]/g, '_'); const ctype = req.headers['content-type'] || 'application/octet-stream'; const localPath = `/tmp/rcpt_${Date.now()}_${filename}`; let fileSize = 0; try { await new Promise((resolve, reject) => { const ws = createWriteStream(localPath); req.on('data', chunk => { fileSize += chunk.length; }); req.pipe(ws); ws.on('finish', resolve); ws.on('error', reject); req.on('error', reject); }); if (fileSize > 12 * 1024 * 1024) { unlinkSync(localPath); return res.status(400).json({ ok: false, error: 'Filen är för stor (max 12 MB)' }); } const s3Key = `receipts/${tenant_id}/${id}/${Date.now()}_${filename}`; const pubUrl = await s3Put(localPath, s3Key); try { unlinkSync(localPath); } catch {} const { rows } = await pool.query( 'SELECT metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2', [id, tenant_id] ); if (!rows.length) return res.status(404).json({ ok: false, error: 'Verifikat ej hittat' }); const meta = rows[0].metadata || {}; const receipts = [...(meta.receipts || []), { s3Key, filename, contentType: ctype, fileSize, url: pubUrl, uploaded_by: req.auth?.user_id || 'unknown', uploaded_at: new Date().toISOString(), }]; await pool.query( `UPDATE ledger_journal_entries SET metadata = metadata || $1::jsonb WHERE id=$2 AND tenant_id=$3`, [JSON.stringify({ receipts }), id, tenant_id] ); res.json({ ok: true, filename, s3Key, url: pubUrl, receipts, count: receipts.length }); } catch (e) { try { unlinkSync(localPath); } catch {} res.status(500).json({ ok: false, error: e.message }); } }); // POST /api/ledger/journal/:id/receipt — bekräfta upload app.post('/api/ledger/journal/:id/receipt', async (req, res) => { const { id } = req.params; const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group'; const { s3Key, filename, contentType, fileSize } = req.body; if (!s3Key || !filename) return res.status(400).json({ ok: false, error: 's3Key och filename krävs' }); try { const { rows } = await pool.query( 'SELECT id, metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2', [id, tenant_id] ); if (!rows.length) return res.status(404).json({ ok: false, error: 'Verifikat ej hittat' }); const meta = rows[0].metadata || {}; const receipts = [...(meta.receipts || []), { s3Key, filename, contentType: contentType || 'application/octet-stream', fileSize: fileSize || 0, url: `https://${RECEIPT_BUCKET}.s3.${RECEIPT_REGION}.amazonaws.com/${s3Key}`, uploaded_by: req.auth?.user_id || 'unknown', uploaded_at: new Date().toISOString(), }]; await pool.query( `UPDATE ledger_journal_entries SET metadata = metadata || $1::jsonb WHERE id=$2 AND tenant_id=$3`, [JSON.stringify({ receipts }), id, tenant_id] ); res.json({ ok: true, receipts, count: receipts.length }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); // GET /api/ledger/journal/:id/receipts app.get('/api/ledger/journal/:id/receipts', async (req, res) => { const { id } = req.params; const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group'; try { const { rows } = await pool.query( 'SELECT metadata FROM ledger_journal_entries WHERE id=$1 AND tenant_id=$2', [id, tenant_id] ); if (!rows.length) return res.status(404).json({ ok: false, error: 'Ej hittat' }); const receipts = (rows[0].metadata || {}).receipts || []; res.json({ ok: true, receipts, count: receipts.length }); } catch (e) { res.status(500).json({ ok: false, error: e.message }); } }); }