fix: BankSyncSection enhancements, merchant rule service updates, user settings import
This commit is contained in:
parent
3745ef79b7
commit
809bd4498b
|
|
@ -311,6 +311,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
||||||
const [btEnabled, setBtEnabled] = useState(false);
|
const [btEnabled, setBtEnabled] = useState(false);
|
||||||
const [btAccountId, setBtAccountId] = useState('');
|
const [btAccountId, setBtAccountId] = useState('');
|
||||||
const [btPendingDays, setBtPendingDays] = useState(3);
|
const [btPendingDays, setBtPendingDays] = useState(3);
|
||||||
|
const [btLateGraceDays, setBtLateGraceDays] = useState(0);
|
||||||
const [btAccounts, setBtAccounts] = useState([]);
|
const [btAccounts, setBtAccounts] = useState([]);
|
||||||
const [btSaving, setBtSaving] = useState(false);
|
const [btSaving, setBtSaving] = useState(false);
|
||||||
|
|
||||||
|
|
@ -339,6 +340,7 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
||||||
setBtEnabled(settings.bank_tracking_enabled === 'true');
|
setBtEnabled(settings.bank_tracking_enabled === 'true');
|
||||||
setBtAccountId(settings.bank_tracking_account_id || '');
|
setBtAccountId(settings.bank_tracking_account_id || '');
|
||||||
setBtPendingDays(parseInt(settings.bank_tracking_pending_days, 10) || 3);
|
setBtPendingDays(parseInt(settings.bank_tracking_pending_days, 10) || 3);
|
||||||
|
setBtLateGraceDays(parseInt(settings.bank_late_attribution_days, 10) || 0);
|
||||||
setBtAccounts(Array.isArray(accounts) ? accounts : []);
|
setBtAccounts(Array.isArray(accounts) ? accounts : []);
|
||||||
} catch {
|
} catch {
|
||||||
// non-fatal — bank tracking section just won't populate
|
// non-fatal — bank tracking section just won't populate
|
||||||
|
|
@ -352,11 +354,13 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
||||||
bank_tracking_enabled: String(patch.enabled ?? btEnabled),
|
bank_tracking_enabled: String(patch.enabled ?? btEnabled),
|
||||||
bank_tracking_account_id: String(patch.accountId ?? btAccountId),
|
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);
|
await api.saveSettings(next);
|
||||||
if (patch.enabled !== undefined) setBtEnabled(patch.enabled);
|
if (patch.enabled !== undefined) setBtEnabled(patch.enabled);
|
||||||
if (patch.accountId !== undefined) setBtAccountId(patch.accountId);
|
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');
|
toast.success('Bank tracking settings saved');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Failed to save bank tracking settings');
|
toast.error(err.message || 'Failed to save bank tracking settings');
|
||||||
|
|
@ -861,6 +865,38 @@ export default function BankSyncSection({ onConnectionChange, cardProps = {} })
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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 */}
|
{/* 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">
|
<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>
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
const { normalizeMerchant } = require('./subscriptionService');
|
const { normalizeMerchant } = require('./subscriptionService');
|
||||||
const { computeBalanceDelta } = require('./billsService');
|
const { computeBalanceDelta } = require('./billsService');
|
||||||
|
const { getUserSettings } = require('./userSettings');
|
||||||
|
|
||||||
// Word-boundary merchant match — requires the rule to appear as complete word(s)
|
// 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.
|
// 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: [] };
|
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 getBill = db.prepare('SELECT current_balance, interest_rate FROM bills WHERE id = ? AND deleted_at IS NULL');
|
||||||
const insertPayment = db.prepare(`
|
const insertPayment = db.prepare(`
|
||||||
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id, balance_delta)
|
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
|
// Check if this payment just missed the previous month's window
|
||||||
const suggestedDate = lateAttributionCandidate(paidDate, rule.due_day);
|
const suggestedDate = lateAttributionCandidate(paidDate, rule.due_day);
|
||||||
if (suggestedDate) {
|
if (suggestedDate) {
|
||||||
if (rule.auto_attribute_late) {
|
const dayOfMonth = new Date(paidDate + 'T00:00:00').getDate();
|
||||||
// Auto-apply without prompting — this bill always posts after month end
|
// 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")
|
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);
|
.run(suggestedDate, tx.id, rule.bill_id);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -244,6 +253,14 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
|
||||||
// Check for late attribution (payment just crossed month boundary)
|
// Check for late attribution (payment just crossed month boundary)
|
||||||
const suggestedDate = billMeta ? lateAttributionCandidate(paidDate, billMeta.due_day) : null;
|
const suggestedDate = billMeta ? lateAttributionCandidate(paidDate, billMeta.due_day) : null;
|
||||||
if (suggestedDate) {
|
if (suggestedDate) {
|
||||||
|
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);
|
const inserted = getPaymentId.get(tx.id, billId);
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
lateAttributions.push({
|
lateAttributions.push({
|
||||||
|
|
@ -257,6 +274,7 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[syncBillPaymentsFromSimplefin] Transaction failed, no payments recorded:', err.message);
|
console.error('[syncBillPaymentsFromSimplefin] Transaction failed, no payments recorded:', err.message);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const USER_SETTING_KEYS = [
|
||||||
'bank_tracking_enabled',
|
'bank_tracking_enabled',
|
||||||
'bank_tracking_account_id',
|
'bank_tracking_account_id',
|
||||||
'bank_tracking_pending_days',
|
'bank_tracking_pending_days',
|
||||||
|
'bank_late_attribution_days',
|
||||||
];
|
];
|
||||||
|
|
||||||
function defaultUserSettings() {
|
function defaultUserSettings() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue