fix: BankSyncSection enhancements, merchant rule service updates, user settings import
This commit is contained in:
parent
3745ef79b7
commit
809bd4498b
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue