fix: BankSyncSection enhancements, merchant rule service updates, user settings import

This commit is contained in:
null 2026-06-04 02:57:09 -05:00
parent 3745ef79b7
commit 809bd4498b
3 changed files with 69 additions and 14 deletions

View File

@ -310,7 +310,8 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
// Bank tracking state
const [btEnabled, setBtEnabled] = useState(false);
const [btAccountId, setBtAccountId] = useState('');
const [btPendingDays, setBtPendingDays] = useState(3);
const [btPendingDays, setBtPendingDays] = useState(3);
const [btLateGraceDays, setBtLateGraceDays] = useState(0);
const [btAccounts, setBtAccounts] = useState([]);
const [btSaving, setBtSaving] = useState(false);
@ -339,6 +340,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
setBtEnabled(settings.bank_tracking_enabled === 'true');
setBtAccountId(settings.bank_tracking_account_id || '');
setBtPendingDays(parseInt(settings.bank_tracking_pending_days, 10) || 3);
setBtLateGraceDays(parseInt(settings.bank_late_attribution_days, 10) || 0);
setBtAccounts(Array.isArray(accounts) ? accounts : []);
} catch {
// non-fatal bank tracking section just won't populate
@ -351,12 +353,14 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
const next = {
bank_tracking_enabled: String(patch.enabled ?? btEnabled),
bank_tracking_account_id: String(patch.accountId ?? btAccountId),
bank_tracking_pending_days: String(patch.pendingDays ?? btPendingDays),
bank_tracking_pending_days: String(patch.pendingDays ?? btPendingDays),
bank_late_attribution_days: String(patch.lateGraceDays ?? btLateGraceDays),
};
await api.saveSettings(next);
if (patch.enabled !== undefined) setBtEnabled(patch.enabled);
if (patch.accountId !== undefined) setBtAccountId(patch.accountId);
if (patch.pendingDays !== undefined) setBtPendingDays(patch.pendingDays);
if (patch.pendingDays !== undefined) setBtPendingDays(patch.pendingDays);
if (patch.lateGraceDays !== undefined) setBtLateGraceDays(patch.lateGraceDays);
toast.success('Bank tracking settings saved');
} catch (err) {
toast.error(err.message || 'Failed to save bank tracking settings');
@ -861,6 +865,38 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
</Select>
</div>
{/* Late payment grace window */}
<div className="space-y-1.5">
<Label className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Late payment grace window
</Label>
<p className="text-xs text-muted-foreground">
If a payment posts in the first N days of a new month but the bill was due in the prior month,
automatically count it for the prior month no prompt needed.
</p>
<Select
value={String(btLateGraceDays)}
onValueChange={v => handleBtSave({ lateGraceDays: parseInt(v, 10) })}
disabled={btSaving}
>
<SelectTrigger className="h-9 w-48 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="0">Off prompt me each time</SelectItem>
<SelectItem value="1">1 day</SelectItem>
<SelectItem value="2">2 days</SelectItem>
<SelectItem value="3">3 days (recommended)</SelectItem>
<SelectItem value="5">5 days</SelectItem>
</SelectContent>
</Select>
{btLateGraceDays > 0 && (
<p className="text-[11px] text-emerald-600 dark:text-emerald-400">
Any payment posting on the 1st{btLateGraceDays}{btLateGraceDays === 1 ? 'st' : btLateGraceDays === 2 ? 'nd' : btLateGraceDays === 3 ? 'rd' : 'th'} will automatically count for the prior month if the bill was due then.
</p>
)}
</div>
{/* Info callout */}
<div className="rounded-lg border border-border/60 bg-muted/30 px-4 py-3 text-xs text-muted-foreground space-y-1">
<p><span className="font-semibold text-foreground">How it works:</span> Your live bank balance is fetched every time your data syncs. Bills you've already marked paid are not double-counted your bank balance reflects them. Only unpaid bills still due this month are subtracted.</p>

View File

@ -2,6 +2,7 @@
const { normalizeMerchant } = require('./subscriptionService');
const { computeBalanceDelta } = require('./billsService');
const { getUserSettings } = require('./userSettings');
// Word-boundary merchant match — requires the rule to appear as complete word(s)
// within the transaction string (or vice versa), not just as a substring.
@ -79,6 +80,10 @@ function applyMerchantRules(db, userId) {
if (txRows.length === 0) return { matched: 0, matched_bills: [] };
// Global grace window — auto-apply late attribution for ALL bills within this many days
const userSettings = (() => { try { return getUserSettings(userId); } catch { return {}; } })();
const globalGraceDays = parseInt(userSettings.bank_late_attribution_days, 10) || 0;
const getBill = db.prepare('SELECT current_balance, interest_rate FROM bills WHERE id = ? AND deleted_at IS NULL');
const insertPayment = db.prepare(`
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id, balance_delta)
@ -123,8 +128,12 @@ function applyMerchantRules(db, userId) {
// Check if this payment just missed the previous month's window
const suggestedDate = lateAttributionCandidate(paidDate, rule.due_day);
if (suggestedDate) {
if (rule.auto_attribute_late) {
// Auto-apply without prompting — this bill always posts after month end
const dayOfMonth = new Date(paidDate + 'T00:00:00').getDate();
// Auto-apply if: per-rule toggle is on OR global grace window covers this day
const autoApply = rule.auto_attribute_late ||
(globalGraceDays > 0 && dayOfMonth <= globalGraceDays);
if (autoApply) {
db.prepare("UPDATE payments SET paid_date = ?, updated_at = datetime('now') WHERE transaction_id = ? AND bill_id = ? AND deleted_at IS NULL")
.run(suggestedDate, tx.id, rule.bill_id);
} else {
@ -244,15 +253,24 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
// Check for late attribution (payment just crossed month boundary)
const suggestedDate = billMeta ? lateAttributionCandidate(paidDate, billMeta.due_day) : null;
if (suggestedDate) {
const inserted = getPaymentId.get(tx.id, billId);
if (inserted) {
lateAttributions.push({
payment_id: inserted.id,
bill_name: billMeta.name || `Bill #${billId}`,
original_date: paidDate,
suggested_date: suggestedDate,
amount,
});
const dayOfMonth = new Date(paidDate + 'T00:00:00').getDate();
const globalDays = (() => { try { return parseInt(getUserSettings(userId).bank_late_attribution_days, 10) || 0; } catch { return 0; } })();
const autoApply = (globalDays > 0 && dayOfMonth <= globalDays);
if (autoApply) {
db.prepare("UPDATE payments SET paid_date = ?, updated_at = datetime('now') WHERE transaction_id = ? AND bill_id = ? AND deleted_at IS NULL")
.run(suggestedDate, tx.id, billId);
} else {
const inserted = getPaymentId.get(tx.id, billId);
if (inserted) {
lateAttributions.push({
payment_id: inserted.id,
bill_name: billMeta.name || `Bill #${billId}`,
original_date: paidDate,
suggested_date: suggestedDate,
amount,
});
}
}
}
}

View File

@ -11,6 +11,7 @@ const USER_SETTING_KEYS = [
'bank_tracking_enabled',
'bank_tracking_account_id',
'bank_tracking_pending_days',
'bank_late_attribution_days',
];
function defaultUserSettings() {