refactor(sync): centralize sync constants in bankSyncConfigService, wire through config/UI
This commit is contained in:
parent
a73d0afe07
commit
a2ac241cd3
|
|
@ -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 1–45 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 & backfill
|
Initial connect & 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's maximum. Requests at this limit may occasionally
|
{maxDays} days is SimpleFIN Bridge'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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 ────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue