← back to AbramsOS

routes/upload.js

76 lines

// Manual CSV upload route — drag-drop a bank-statement CSV → purchase rows.

const express = require('express');
const multer = require('multer');
const db = require('../lib/db');
const audit = require('../lib/audit');
const { id } = require('../lib/ids');
const csvParser = require('../lib/csv-parser');

const router = express.Router();
const DEV_USER_ID = 'user_steve';

// In-memory storage; file size cap 10 MB; keep CSV in RAM not disk.
const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 10 * 1024 * 1024 },
});

router.post('/api/upload/csv', upload.single('file'), async (req, res) => {
  if (!req.file) return res.status(400).json({ error: 'no file uploaded; use multipart/form-data with field name "file"' });
  const csvText = req.file.buffer.toString('utf-8');

  const parsed = csvParser.parseStatement(csvText);
  if (parsed.errors.length && parsed.rows.length === 0) {
    return res.status(400).json({ error: 'csv parse failed', details: parsed.errors });
  }

  let inserted = 0;
  for (const r of parsed.rows) {
    const purchaseId = id('purchase');
    // Stable dedup key so re-uploading overlapping statements doesn't double rows.
    const dedupKey = `csv:${new Date(r.date).toISOString().slice(0, 10)}:${r.amount}:${r.merchant}`.slice(0, 200);
    try {
      const ins = 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, NULL, $4, $5, $6, 'USD', $7, $8)
         ON CONFLICT (user_id, order_number) WHERE order_number IS NOT NULL DO NOTHING
         RETURNING id`,
        [purchaseId, DEV_USER_ID, r.merchant, dedupKey, r.date, r.amount, 0.85, r]
      );
      if (!ins.rows.length) continue;  // duplicate of an already-imported transaction
      await audit.log({
        actorType: 'user',
        actorId: DEV_USER_ID,
        objectType: 'purchase',
        objectId: purchaseId,
        eventType: 'purchase_extracted',
        metadata: { source: 'csv_upload', merchant: r.merchant, total: r.amount, line: r.raw.line, filename: req.file.originalname },
      });
      inserted += 1;
    } catch (err) {
      console.warn('[csv-upload] insert', err.message);
    }
  }

  await audit.log({
    actorType: 'user',
    actorId: DEV_USER_ID,
    objectType: 'user_account',
    objectId: DEV_USER_ID,
    eventType: 'csv_upload',
    metadata: { filename: req.file.originalname, size: req.file.size, rows: parsed.rows.length, inserted, skipped: parsed.skipped },
  });

  res.json({
    ok: true,
    filename: req.file.originalname,
    inserted,
    skipped: parsed.skipped,
    parse_errors: parsed.errors.slice(0, 5),
  });
});

module.exports = router;