BillTracker/routes/dataSources.js

135 lines
5.5 KiB
JavaScript

'use strict';
const router = require('express').Router();
const { getDb } = require('../db/database');
const { standardizeError } = require('../middleware/errorFormatter');
const { decorateDataSource, ensureManualDataSource } = require('../services/transactionService');
const { connectSimplefin, syncDataSource, disconnectDataSource } = require('../services/bankSyncService');
const { sanitizeErrorMessage } = require('../services/simplefinService');
const { getBankSyncConfig } = require('../services/bankSyncConfigService');
const VALID_TYPES = new Set(['manual', 'file_import', 'provider_sync']);
const VALID_STATUSES = new Set(['active', 'inactive', 'error']);
function cleanFilter(value) {
return typeof value === 'string' ? value.trim() : '';
}
function safeError(err, fallback) {
const msg = sanitizeErrorMessage(err?.message || fallback);
const status = typeof err?.status === 'number' ? err.status : 500;
return { msg, status };
}
// ─── GET /api/data-sources ────────────────────────────────────────────────────
router.get('/', (req, res) => {
const db = getDb();
ensureManualDataSource(db, req.user.id);
const type = cleanFilter(req.query.type);
const status = cleanFilter(req.query.status);
if (type && !VALID_TYPES.has(type)) return res.status(400).json(standardizeError('type must be manual, file_import, or provider_sync', 'VALIDATION_ERROR', 'type'));
if (status && !VALID_STATUSES.has(status)) return res.status(400).json(standardizeError('status must be active, inactive, or error', 'VALIDATION_ERROR', 'status'));
let query = `
SELECT
ds.id, ds.user_id, ds.type, ds.provider, ds.name, ds.status,
ds.config_json, ds.last_sync_at, ds.last_error, ds.created_at, ds.updated_at,
COUNT(DISTINCT fa.id) AS account_count,
COUNT(DISTINCT t.id) AS transaction_count
FROM data_sources ds
LEFT JOIN financial_accounts fa ON fa.data_source_id = ds.id AND fa.user_id = ds.user_id
LEFT JOIN transactions t ON t.data_source_id = ds.id AND t.user_id = ds.user_id
WHERE ds.user_id = ?
`;
const params = [req.user.id];
if (type) { query += ' AND ds.type = ?'; params.push(type); }
if (status) { query += ' AND ds.status = ?'; params.push(status); }
query += `
GROUP BY ds.id
ORDER BY
CASE WHEN ds.type = 'manual' THEN 0 ELSE 1 END,
ds.name COLLATE NOCASE ASC
`;
res.json(db.prepare(query).all(...params).map(decorateDataSource));
});
// ─── GET /api/data-sources/simplefin/status ──────────────────────────────────
router.get('/simplefin/status', (req, res) => {
const { enabled } = getBankSyncConfig();
res.json({ enabled });
});
// ─── POST /api/data-sources/simplefin/connect ────────────────────────────────
router.post('/simplefin/connect', async (req, res) => {
if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
}
const setupToken = typeof req.body?.setupToken === 'string' ? req.body.setupToken.trim() : '';
if (!setupToken) {
return res.status(400).json(standardizeError('setupToken is required', 'VALIDATION_ERROR', 'setupToken'));
}
try {
const db = getDb();
const result = await connectSimplefin(db, req.user.id, setupToken);
res.status(201).json(result);
} catch (err) {
const { msg, status } = safeError(err, 'Failed to connect SimpleFIN');
res.status(status).json(standardizeError(msg, err?.code || 'SIMPLEFIN_ERROR'));
}
});
// ─── POST /api/data-sources/:id/sync ─────────────────────────────────────────
router.post('/:id/sync', async (req, res) => {
if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
}
const id = parseInt(req.params.id, 10);
if (!Number.isInteger(id) || id < 1) {
return res.status(400).json(standardizeError('Invalid data source id', 'VALIDATION_ERROR', 'id'));
}
try {
const db = getDb();
const result = await syncDataSource(db, req.user.id, id);
res.json(result);
} catch (err) {
const { msg, status } = safeError(err, 'Sync failed');
res.status(status).json(standardizeError(msg, err?.code || 'SIMPLEFIN_ERROR'));
}
});
// ─── DELETE /api/data-sources/:id ────────────────────────────────────────────
router.delete('/:id', (req, res) => {
if (!getBankSyncConfig().enabled) {
return res.status(503).json(standardizeError('Bank sync is not enabled on this server', 'BANK_SYNC_DISABLED'));
}
const id = parseInt(req.params.id, 10);
if (!Number.isInteger(id) || id < 1) {
return res.status(400).json(standardizeError('Invalid data source id', 'VALIDATION_ERROR', 'id'));
}
try {
disconnectDataSource(getDb(), req.user.id, id);
res.json({ ok: true });
} catch (err) {
const { msg, status } = safeError(err, 'Failed to disconnect');
res.status(status).json(standardizeError(msg, err?.code || 'DISCONNECT_ERROR'));
}
});
module.exports = router;