'use strict'; const express = require('express'); const { standardizeError } = require('../middleware/errorFormatter'); const router = express.Router(); const { previewSpreadsheet, applyImportDecisions, getImportHistory, } = require('../services/spreadsheetImportService'); const { previewUserDbImport, applyUserDbImport, } = require('../services/userDbImportService'); function makeErrorId() { return `imp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`; } function sendImportError(res, err, fallback, defaultCode) { if (err.status) { return res.status(err.status).json({ error: fallback, message: err.message, code: err.code || defaultCode, details: Array.isArray(err.details) ? err.details : [], }); } // Log error ID server-side only — never expose to clients const errorId = makeErrorId(); console.error(`[import] ${fallback} (${errorId}):`, err.stack || err.message); return res.status(500).json({ error: fallback, message: 'Unexpected import server error. Please try again or adjust the import decisions.', code: defaultCode, }); } // ─── POST /api/import/spreadsheet/preview ───────────────────────────────────── // Accepts an XLSX file as raw binary body. // Send with Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet // or application/octet-stream. Optionally pass X-Filename header for audit logging. // Returns a preview with proposed row mappings and bill matches; writes no data. router.post( '/spreadsheet/preview', express.raw({ type: [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/octet-stream', 'application/xlsx', 'application/vnd.ms-excel', ], limit: '10mb', }), async (req, res) => { try { if (!Buffer.isBuffer(req.body) || req.body.length === 0) { return res.status(400).json({ error: 'XLSX file is required. Send as raw binary with Content-Type application/octet-stream or the XLSX MIME type.', }); } const rawFilename = req.headers['x-filename']; const originalFilename = rawFilename ? rawFilename.replace(/[^a-zA-Z0-9._\-\s]/g, '').trim().slice(0, 255) : null; const options = { sheet_name: req.query.sheet || null, parse_all_sheets: req.query.parse_all_sheets === 'true', default_year: req.query.year ? parseInt(req.query.year, 10) : null, default_month: req.query.month ? parseInt(req.query.month, 10) : null, original_filename: originalFilename, }; if (options.default_year && (options.default_year < 2000 || options.default_year > 2100)) { return res.status(400).json(standardizeError('year must be a 4-digit integer between 2000 and 2100', 'VALIDATION_ERROR', 'year')); } if (options.default_month && (options.default_month < 1 || options.default_month > 12)) { return res.status(400).json(standardizeError('month must be an integer between 1 and 12', 'VALIDATION_ERROR', 'month')); } const result = await previewSpreadsheet(req.user.id, req.body, options); res.json(result); } catch (err) { return sendImportError(res, err, 'Import preview failed', 'IMPORT_PREVIEW_ERROR'); } }, ); // ─── POST /api/import/spreadsheet/apply ────────────────────────────────────── // Applies confirmed import decisions from a previous preview session. // Body: { import_session_id, decisions: [...], options: { overwrite: false } } // Each decision must have: row_id, action, and action-specific fields. // Only writes data for explicitly confirmed decisions; skips ambiguous rows. router.post('/spreadsheet/apply', express.json({ limit: '2mb' }), async (req, res) => { try { const { import_session_id, decisions, options } = req.body || {}; if (!import_session_id || typeof import_session_id !== 'string') { return res.status(400).json(standardizeError('import_session_id is required', 'VALIDATION_ERROR', 'import_session_id')); } if (!Array.isArray(decisions) || decisions.length === 0) { return res.status(400).json(standardizeError('decisions array is required and must not be empty', 'VALIDATION_ERROR', 'decisions')); } if (decisions.length > 5000) { return res.status(400).json(standardizeError('Too many decisions in a single apply request (max 5000)', 'VALIDATION_ERROR', 'decisions')); } const result = await applyImportDecisions( req.user.id, import_session_id, decisions, options || {}, ); res.json(result); } catch (err) { return sendImportError(res, err, 'Import apply failed', 'IMPORT_APPLY_ERROR'); } }); // ─── POST /api/import/user-db/preview ──────────────────────────────────────── // Accepts a SQLite user data export created by this app. This is not an admin // full-database restore and writes no live bill/payment/category data. router.post( '/user-db/preview', express.raw({ type: [ 'application/octet-stream', 'application/x-sqlite3', 'application/vnd.sqlite3', 'application/x-sqlite', 'application/vnd.sqlite', ], limit: '50mb', }), async (req, res) => { try { const rawFilename = req.headers['x-filename']; const originalFilename = rawFilename ? rawFilename.replace(/[^a-zA-Z0-9._\-\s]/g, '').trim().slice(0, 255) : null; const result = await previewUserDbImport(req.user.id, req.body, { original_filename: originalFilename }); res.json(result); } catch (err) { return sendImportError(res, err, 'User SQLite import preview failed', 'USER_DB_IMPORT_PREVIEW_ERROR'); } }, ); // ─── POST /api/import/user-db/apply ────────────────────────────────────────── // Applies a previously previewed user SQLite export session. User ownership is // derived from req.user only; existing data is skipped by default. router.post('/user-db/apply', express.json({ limit: '1mb' }), async (req, res) => { try { const { import_session_id, options } = req.body || {}; if (!import_session_id || typeof import_session_id !== 'string') { return res.status(400).json(standardizeError('import_session_id is required', 'VALIDATION_ERROR', 'import_session_id')); } const result = await applyUserDbImport(req.user.id, import_session_id, options || {}); res.json(result); } catch (err) { return sendImportError(res, err, 'User SQLite import apply failed', 'USER_DB_IMPORT_APPLY_ERROR'); } }); // ─── GET /api/import/history ────────────────────────────────────────────────── // Returns the authenticated user's import history (last 100 imports). router.get('/history', (req, res) => { try { const history = getImportHistory(req.user.id); res.json({ history }); } catch (err) { console.error('[import] history error:', err.message); res.status(500).json({ error: 'Failed to load import history' }); } }); module.exports = router;