refactor(sync): centralize sync constants in bankSyncConfigService, wire through config/UI

This commit is contained in:
null 2026-06-06 15:51:56 -05:00
parent a73d0afe07
commit a2ac241cd3
5 changed files with 39 additions and 31 deletions

View File

@ -32,7 +32,7 @@ export default function BankSyncAdminCard() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [enabled, setEnabled] = useState(false); const [enabled, setEnabled] = useState(false);
const [syncInterval, setSyncInterval] = useState(4); const [syncInterval, setSyncInterval] = useState(4);
const [syncDays, setSyncDays] = useState(90); const [syncDays, setSyncDays] = useState(30);
useEffect(() => { useEffect(() => {
api.bankSyncConfig() api.bankSyncConfig()
@ -40,7 +40,7 @@ export default function BankSyncAdminCard() {
setConfig(d); setConfig(d);
setEnabled(!!d.enabled); setEnabled(!!d.enabled);
setSyncInterval(d.sync_interval_hours ?? 4); setSyncInterval(d.sync_interval_hours ?? 4);
setSyncDays(d.sync_days ?? 90); setSyncDays(d.sync_days ?? 30);
}) })
.catch(err => setLoadError(err.message || 'Failed to load bank sync config')) .catch(err => setLoadError(err.message || 'Failed to load bank sync config'))
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@ -49,12 +49,13 @@ export default function BankSyncAdminCard() {
const handleSave = async () => { const handleSave = async () => {
const hours = parseFloat(syncInterval); const hours = parseFloat(syncInterval);
const days = parseInt(syncDays, 10); const days = parseInt(syncDays, 10);
const maxDays = config?.sync_days_max ?? 45;
if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) { if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) {
toast.error('Sync interval must be between 0.5 and 168 hours.'); toast.error('Sync interval must be between 0.5 and 168 hours.');
return; return;
} }
if (!Number.isFinite(days) || days < 1 || days > 45) { if (!Number.isFinite(days) || days < 1 || days > maxDays) {
toast.error('Routine sync lookback must be 145 days. SimpleFIN Bridge enforces a 45-day hard limit — values above 45 return errors.'); toast.error(`Routine sync lookback must be 1${maxDays} days. SimpleFIN Bridge enforces a ${maxDays}-day hard limit — values above ${maxDays} return errors.`);
return; return;
} }
setSaving(true); setSaving(true);
@ -63,7 +64,7 @@ export default function BankSyncAdminCard() {
setConfig(result); setConfig(result);
setEnabled(!!result.enabled); setEnabled(!!result.enabled);
setSyncInterval(result.sync_interval_hours ?? 4); setSyncInterval(result.sync_interval_hours ?? 4);
setSyncDays(result.sync_days ?? 90); setSyncDays(result.sync_days ?? 30);
toast.success('Bank sync settings saved.'); toast.success('Bank sync settings saved.');
} catch (err) { } catch (err) {
toast.error(err.message || 'Failed to update bank sync setting.'); toast.error(err.message || 'Failed to update bank sync setting.');
@ -96,6 +97,8 @@ export default function BankSyncAdminCard() {
|| parseFloat(syncInterval) !== config?.sync_interval_hours || parseFloat(syncInterval) !== config?.sync_interval_hours
|| parseInt(syncDays, 10) !== config?.sync_days; || parseInt(syncDays, 10) !== config?.sync_days;
const worker = config?.worker; const worker = config?.worker;
const seedDays = config?.seed_days ?? 44;
const maxDays = config?.sync_days_max ?? 45;
return ( return (
<Card> <Card>
@ -161,12 +164,13 @@ export default function BankSyncAdminCard() {
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide"> <p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Initial connect &amp; backfill Initial connect &amp; backfill
</p> </p>
<span className="font-mono text-sm font-bold">6 days</span> <span className="font-mono text-sm font-bold">{seedDays} days</span>
</div> </div>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
The first sync (and any manual backfill) always fetches the maximum 60 days of history The first sync (and any manual backfill) fetches up to <strong>{seedDays} days</strong> of
to build a complete transaction picture. This is fixed SimpleFIN Bridge enforces a history to build a complete transaction picture. This is fixed SimpleFIN Bridge enforces a
strict <strong>60-day hard limit</strong> and will return possible errors. strict <strong>{maxDays}-day hard limit</strong>, so this stays one day under it to avoid
latency-related errors.
</p> </p>
</div> </div>
@ -184,10 +188,10 @@ export default function BankSyncAdminCard() {
<Input <Input
type="number" type="number"
min="1" min="1"
max="45" max={maxDays}
step="1" step="1"
value={syncDays} value={syncDays}
onChange={e => setSyncDays(Math.min(45, Math.max(1, parseInt(e.target.value, 10) || 30)))} onChange={e => setSyncDays(Math.min(maxDays, Math.max(1, parseInt(e.target.value, 10) || 30)))}
className="w-20 text-sm text-right" className="w-20 text-sm text-right"
/> />
<span className="text-sm text-muted-foreground">days</span> <span className="text-sm text-muted-foreground">days</span>
@ -195,11 +199,11 @@ export default function BankSyncAdminCard() {
</div> </div>
{/* Amber warning at the SimpleFIN limit */} {/* Amber warning at the SimpleFIN limit */}
{parseInt(syncDays, 10) >= 45 && ( {parseInt(syncDays, 10) >= maxDays && (
<div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/8 px-3 py-2.5"> <div className="flex items-start gap-2 rounded-lg border border-amber-500/30 bg-amber-500/8 px-3 py-2.5">
<AlertTriangle className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" /> <AlertTriangle className="h-3.5 w-3.5 text-amber-500 shrink-0 mt-0.5" />
<p className="text-xs text-amber-600 dark:text-amber-400"> <p className="text-xs text-amber-600 dark:text-amber-400">
45 days is SimpleFIN Bridge&apos;s maximum. Requests at this limit may occasionally {maxDays} days is SimpleFIN Bridge&apos;s maximum. Requests at this limit may occasionally
fail due to request latency 30 days or less is recommended for reliable routine syncs. fail due to request latency 30 days or less is recommended for reliable routine syncs.
</p> </p>
</div> </div>
@ -209,8 +213,8 @@ export default function BankSyncAdminCard() {
<div className="flex items-start gap-2 text-xs text-muted-foreground"> <div className="flex items-start gap-2 text-xs text-muted-foreground">
<Info className="h-3.5 w-3.5 shrink-0 mt-0.5" /> <Info className="h-3.5 w-3.5 shrink-0 mt-0.5" />
<span> <span>
SimpleFIN Bridge enforces a <strong>45-day maximum</strong> on all requests. SimpleFIN Bridge enforces a <strong>{maxDays}-day maximum</strong> on all requests.
Any value above 45 will cause sync errors for all users. Any value above {maxDays} will cause sync errors for all users.
</span> </span>
</div> </div>
</div> </div>

View File

@ -289,7 +289,8 @@ function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonit
export default function BankSyncSection({ onConnectionChange, cardProps = {} }) { export default function BankSyncSection({ onConnectionChange, cardProps = {} }) {
const [enabled, setEnabled] = useState(null); const [enabled, setEnabled] = useState(null);
const [syncDays, setSyncDays] = useState(90); const [syncDays, setSyncDays] = useState(30);
const [seedDays, setSeedDays] = useState(44);
const [connections, setConnections] = useState([]); const [connections, setConnections] = useState([]);
const [accountsBySource, setAccountsBySource] = useState({}); const [accountsBySource, setAccountsBySource] = useState({});
const [accountsLoading, setAccountsLoading] = useState({}); const [accountsLoading, setAccountsLoading] = useState({});
@ -377,7 +378,8 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
api.dataSources({ type: 'provider_sync' }), api.dataSources({ type: 'provider_sync' }),
]); ]);
setEnabled(status.enabled); setEnabled(status.enabled);
setSyncDays(status.sync_days ?? 90); setSyncDays(status.sync_days ?? 30);
setSeedDays(status.seed_days ?? 44);
const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : []; const conns = Array.isArray(sources) ? sources.filter(s => s.provider === 'simplefin') : [];
setConnections(conns); setConnections(conns);
onConnectionChange?.(conns[0] || null); onConnectionChange?.(conns[0] || null);
@ -484,7 +486,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
if (result.errlist) { if (result.errlist) {
toast.warning(`Backfill complete — ${result.transactionsNew} new transaction(s). Some connections need attention: ${result.errlist}`); toast.warning(`Backfill complete — ${result.transactionsNew} new transaction(s). Some connections need attention: ${result.errlist}`);
} else { } else {
toast.success(`Backfill complete — ${result.transactionsNew} new transaction(s) pulled from the last 90 days.`); toast.success(`Backfill complete — ${result.transactionsNew} new transaction(s) pulled from the last ${seedDays} days.`);
} }
await load(); await load();
} catch (err) { } catch (err) {
@ -632,11 +634,11 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
onClick={() => handleBackfill(conn.id)} onClick={() => handleBackfill(conn.id)}
disabled={syncing === conn.id || backfilling === conn.id} disabled={syncing === conn.id || backfilling === conn.id}
className="h-8 text-xs gap-1.5 text-muted-foreground" className="h-8 text-xs gap-1.5 text-muted-foreground"
title="Pull up to 90 days of transaction history" title={`Pull up to ${seedDays} days of transaction history`}
> >
{backfilling === conn.id {backfilling === conn.id
? <><Loader2 className="h-3.5 w-3.5 animate-spin" />Backfilling</> ? <><Loader2 className="h-3.5 w-3.5 animate-spin" />Backfilling</>
: <><History className="h-3.5 w-3.5" />90d Backfill</>} : <><History className="h-3.5 w-3.5" />{seedDays}d Backfill</>}
</Button> </Button>
<Button <Button
size="sm" variant="ghost" size="sm" variant="ghost"

View File

@ -91,7 +91,7 @@ router.get('/', (req, res) => {
// ─── GET /api/data-sources/simplefin/status ────────────────────────────────── // ─── GET /api/data-sources/simplefin/status ──────────────────────────────────
router.get('/simplefin/status', (req, res) => { router.get('/simplefin/status', (req, res) => {
const { enabled, sync_days } = getBankSyncConfig(); const { enabled, sync_days, seed_days } = getBankSyncConfig();
const db = getDb(); const db = getDb();
const hasConnections = !!db.prepare( const hasConnections = !!db.prepare(
@ -102,7 +102,7 @@ router.get('/simplefin/status', (req, res) => {
'SELECT 1 FROM bill_merchant_rules WHERE user_id = ? LIMIT 1' 'SELECT 1 FROM bill_merchant_rules WHERE user_id = ? LIMIT 1'
).get(req.user.id); ).get(req.user.id);
res.json({ enabled, sync_days, has_connections: hasConnections, has_merchant_rules: hasMerchantRules }); res.json({ enabled, sync_days, seed_days, has_connections: hasConnections, has_merchant_rules: hasMerchantRules });
}); });
// ─── POST /api/data-sources/simplefin/connect ──────────────────────────────── // ─── POST /api/data-sources/simplefin/connect ────────────────────────────────

View File

@ -40,6 +40,8 @@ function getBankSyncConfig() {
return { return {
enabled, enabled,
sync_days: syncDays, sync_days: syncDays,
seed_days: SYNC_DAYS_EFFECTIVE, // initial connect / explicit backfill window
sync_days_max: SYNC_DAYS_MAX, // SimpleFIN Bridge hard limit
sync_interval_hours: syncIntervalHours, sync_interval_hours: syncIntervalHours,
}; };
} }
@ -67,4 +69,7 @@ function setSyncDays(days) {
return getBankSyncConfig(); return getBankSyncConfig();
} }
module.exports = { getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays }; module.exports = {
getBankSyncConfig, setBankSyncEnabled, setSyncIntervalHours, setSyncDays,
SYNC_DAYS_MAX, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT,
};

View File

@ -8,15 +8,12 @@ const {
normalizeTransaction, normalizeTransaction,
sanitizeErrorMessage, sanitizeErrorMessage,
} = require('./simplefinService'); } = require('./simplefinService');
const { getBankSyncConfig } = require('./bankSyncConfigService'); const { getBankSyncConfig, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT } = require('./bankSyncConfigService');
const { decorateDataSource } = require('./transactionService'); const { decorateDataSource } = require('./transactionService');
const { applyMerchantRules } = require('./billMerchantRuleService'); const { applyMerchantRules } = require('./billMerchantRuleService');
const { applySpendingCategoryRules } = require('./spendingService'); const { applySpendingCategoryRules } = require('./spendingService');
const { autoMatchForUser } = require('./matchSuggestionService'); const { autoMatchForUser } = require('./matchSuggestionService');
const SEED_SYNC_DAYS = 44; // Initial connect / explicit backfill (SimpleFIN Bridge 45-day cap, 1-day buffer)
const ROUTINE_SYNC_DAYS = 30; // Fallback if admin config is missing
function sinceEpochDays(days) { function sinceEpochDays(days) {
return Math.floor((Date.now() - days * 86400 * 1000) / 1000); return Math.floor((Date.now() - days * 86400 * 1000) / 1000);
} }
@ -86,10 +83,10 @@ async function runSync(db, userId, dataSource, { days } = {}) {
const accessUrl = decryptSecret(dataSource.encrypted_secret); const accessUrl = decryptSecret(dataSource.encrypted_secret);
const isFirstSync = !dataSource.last_sync_at; const isFirstSync = !dataSource.last_sync_at;
// Explicit `days` param (e.g. backfill) takes precedence. // Explicit `days` param (e.g. backfill) takes precedence.
// Initial seed always uses the full SEED_SYNC_DAYS window regardless of admin config. // Initial seed always uses the full SYNC_DAYS_EFFECTIVE window regardless of admin config.
// Routine syncs use the admin-configured sync_days (default 30); falls back to ROUTINE_SYNC_DAYS. // Routine syncs use the admin-configured sync_days (default 30); falls back to SYNC_DAYS_DEFAULT.
const config = getBankSyncConfig(); const config = getBankSyncConfig();
const syncDays = days ?? (isFirstSync ? SEED_SYNC_DAYS : (config.sync_days || ROUTINE_SYNC_DAYS)); const syncDays = days ?? (isFirstSync ? SYNC_DAYS_EFFECTIVE : (config.sync_days || SYNC_DAYS_DEFAULT));
const since = sinceEpochDays(syncDays); const since = sinceEpochDays(syncDays);
const raw = await fetchAccountsAndTransactions(accessUrl, since); const raw = await fetchAccountsAndTransactions(accessUrl, since);
@ -211,7 +208,7 @@ async function backfillDataSource(db, userId, dataSourceId) {
let syncResult; let syncResult;
try { try {
syncResult = await runSync(db, userId, dataSource, { days: SEED_SYNC_DAYS }); syncResult = await runSync(db, userId, dataSource, { days: SYNC_DAYS_EFFECTIVE });
} catch (err) { } catch (err) {
const msg = safeErrorMessage(err); const msg = safeErrorMessage(err);
db.prepare(` db.prepare(`