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 [enabled, setEnabled] = useState(false);
const [syncInterval, setSyncInterval] = useState(4);
const [syncDays, setSyncDays] = useState(90);
const [syncDays, setSyncDays] = useState(30);
useEffect(() => {
api.bankSyncConfig()
@ -40,7 +40,7 @@ export default function BankSyncAdminCard() {
setConfig(d);
setEnabled(!!d.enabled);
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'))
.finally(() => setLoading(false));
@ -49,12 +49,13 @@ export default function BankSyncAdminCard() {
const handleSave = async () => {
const hours = parseFloat(syncInterval);
const days = parseInt(syncDays, 10);
const maxDays = config?.sync_days_max ?? 45;
if (!Number.isFinite(hours) || hours < 0.5 || hours > 168) {
toast.error('Sync interval must be between 0.5 and 168 hours.');
return;
}
if (!Number.isFinite(days) || days < 1 || days > 45) {
toast.error('Routine sync lookback must be 145 days. SimpleFIN Bridge enforces a 45-day hard limit — values above 45 return errors.');
if (!Number.isFinite(days) || days < 1 || days > maxDays) {
toast.error(`Routine sync lookback must be 1${maxDays} days. SimpleFIN Bridge enforces a ${maxDays}-day hard limit — values above ${maxDays} return errors.`);
return;
}
setSaving(true);
@ -63,7 +64,7 @@ export default function BankSyncAdminCard() {
setConfig(result);
setEnabled(!!result.enabled);
setSyncInterval(result.sync_interval_hours ?? 4);
setSyncDays(result.sync_days ?? 90);
setSyncDays(result.sync_days ?? 30);
toast.success('Bank sync settings saved.');
} catch (err) {
toast.error(err.message || 'Failed to update bank sync setting.');
@ -96,6 +97,8 @@ export default function BankSyncAdminCard() {
|| parseFloat(syncInterval) !== config?.sync_interval_hours
|| parseInt(syncDays, 10) !== config?.sync_days;
const worker = config?.worker;
const seedDays = config?.seed_days ?? 44;
const maxDays = config?.sync_days_max ?? 45;
return (
<Card>
@ -161,12 +164,13 @@ export default function BankSyncAdminCard() {
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Initial connect &amp; backfill
</p>
<span className="font-mono text-sm font-bold">6 days</span>
<span className="font-mono text-sm font-bold">{seedDays} days</span>
</div>
<p className="text-xs text-muted-foreground">
The first sync (and any manual backfill) always fetches the maximum 60 days of 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.
The first sync (and any manual backfill) fetches up to <strong>{seedDays} days</strong> of
history to build a complete transaction picture. This is fixed SimpleFIN Bridge enforces a
strict <strong>{maxDays}-day hard limit</strong>, so this stays one day under it to avoid
latency-related errors.
</p>
</div>
@ -184,10 +188,10 @@ export default function BankSyncAdminCard() {
<Input
type="number"
min="1"
max="45"
max={maxDays}
step="1"
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"
/>
<span className="text-sm text-muted-foreground">days</span>
@ -195,11 +199,11 @@ export default function BankSyncAdminCard() {
</div>
{/* 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">
<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">
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.
</p>
</div>
@ -209,8 +213,8 @@ export default function BankSyncAdminCard() {
<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" />
<span>
SimpleFIN Bridge enforces a <strong>45-day maximum</strong> on all requests.
Any value above 45 will cause sync errors for all users.
SimpleFIN Bridge enforces a <strong>{maxDays}-day maximum</strong> on all requests.
Any value above {maxDays} will cause sync errors for all users.
</span>
</div>
</div>

View File

@ -289,7 +289,8 @@ function AccountRow({ account, sourceId, expanded, onToggleExpand, onToggleMonit
export default function BankSyncSection({ onConnectionChange, cardProps = {} }) {
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 [accountsBySource, setAccountsBySource] = useState({});
const [accountsLoading, setAccountsLoading] = useState({});
@ -377,7 +378,8 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
api.dataSources({ type: 'provider_sync' }),
]);
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') : [];
setConnections(conns);
onConnectionChange?.(conns[0] || null);
@ -484,7 +486,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
if (result.errlist) {
toast.warning(`Backfill complete — ${result.transactionsNew} new transaction(s). Some connections need attention: ${result.errlist}`);
} 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();
} catch (err) {
@ -632,11 +634,11 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
onClick={() => handleBackfill(conn.id)}
disabled={syncing === conn.id || backfilling === conn.id}
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
? <><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
size="sm" variant="ghost"

View File

@ -91,7 +91,7 @@ router.get('/', (req, res) => {
// ─── GET /api/data-sources/simplefin/status ──────────────────────────────────
router.get('/simplefin/status', (req, res) => {
const { enabled, sync_days } = getBankSyncConfig();
const { enabled, sync_days, seed_days } = getBankSyncConfig();
const db = getDb();
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'
).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 ────────────────────────────────

View File

@ -40,6 +40,8 @@ function getBankSyncConfig() {
return {
enabled,
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,
};
}
@ -67,4 +69,7 @@ function setSyncDays(days) {
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,
sanitizeErrorMessage,
} = require('./simplefinService');
const { getBankSyncConfig } = require('./bankSyncConfigService');
const { getBankSyncConfig, SYNC_DAYS_EFFECTIVE, SYNC_DAYS_DEFAULT } = require('./bankSyncConfigService');
const { decorateDataSource } = require('./transactionService');
const { applyMerchantRules } = require('./billMerchantRuleService');
const { applySpendingCategoryRules } = require('./spendingService');
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) {
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 isFirstSync = !dataSource.last_sync_at;
// Explicit `days` param (e.g. backfill) takes precedence.
// Initial seed always uses the full SEED_SYNC_DAYS window regardless of admin config.
// Routine syncs use the admin-configured sync_days (default 30); falls back to ROUTINE_SYNC_DAYS.
// 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 SYNC_DAYS_DEFAULT.
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 raw = await fetchAccountsAndTransactions(accessUrl, since);
@ -211,7 +208,7 @@ async function backfillDataSource(db, userId, dataSourceId) {
let syncResult;
try {
syncResult = await runSync(db, userId, dataSource, { days: SEED_SYNC_DAYS });
syncResult = await runSync(db, userId, dataSource, { days: SYNC_DAYS_EFFECTIVE });
} catch (err) {
const msg = safeErrorMessage(err);
db.prepare(`