fix(match-suggestions): use rejection timestamps and share late attribution helper

This commit is contained in:
null 2026-06-06 14:06:28 -05:00
parent 99abca9868
commit a97d656e92
3 changed files with 20 additions and 29 deletions

View File

@ -16,6 +16,18 @@ function merchantMatches(txMerchant, ruleMerchant) {
wordBoundary(txMerchant).test(ruleMerchant); wordBoundary(txMerchant).test(ruleMerchant);
} }
// Detects when a payment posted just after month end but the bill was due in the
// prior month. Grace window: up to `graceDays` days into the new month.
// Module-scoped so both applyMerchantRules and syncBillPaymentsFromSimplefin can use it.
function lateAttributionCandidate(paidDateStr, dueDayOfMonth, graceDays = 5) {
const paid = new Date(paidDateStr + 'T00:00:00');
const dayOfMonth = paid.getDate();
if (dayOfMonth > graceDays) return null;
const prevMonthLastDay = new Date(paid.getFullYear(), paid.getMonth(), 0);
if (dueDayOfMonth > prevMonthLastDay.getDate()) return null;
return prevMonthLastDay.toISOString().slice(0, 10); // suggested prior-month date
}
// Persist a merchant→bill rule so future synced transactions auto-match. // Persist a merchant→bill rule so future synced transactions auto-match.
function addMerchantRule(db, userId, billId, merchant) { function addMerchantRule(db, userId, billId, merchant) {
const normalized = normalizeMerchant(merchant); const normalized = normalizeMerchant(merchant);
@ -35,17 +47,6 @@ function addMerchantRule(db, userId, billId, merchant) {
// merchant rules, create payments, and mark the transactions matched. // merchant rules, create payments, and mark the transactions matched.
// Returns { matched: number }. // Returns { matched: number }.
function applyMerchantRules(db, userId) { function applyMerchantRules(db, userId) {
// Detects when a payment posted just after month end but the bill was due in the prior month.
// Grace window: up to LATE_ATTR_DAYS days into the new month.
function lateAttributionCandidate(paidDateStr, dueDayOfMonth, graceDays = 5) {
const paid = new Date(paidDateStr + 'T00:00:00');
const dayOfMonth = paid.getDate();
if (dayOfMonth > graceDays) return null;
const prevMonthLastDay = new Date(paid.getFullYear(), paid.getMonth(), 0);
if (dueDayOfMonth > prevMonthLastDay.getDate()) return null;
return prevMonthLastDay.toISOString().slice(0, 10); // suggested prior-month date
}
let rules; let rules;
try { try {
rules = db.prepare(` rules = db.prepare(`

View File

@ -218,7 +218,7 @@ async function runAllCleanup() {
// Prune match suggestion rejections older than 90 days // Prune match suggestion rejections older than 90 days
try { try {
const { changes } = getDb().prepare( const { changes } = getDb().prepare(
"DELETE FROM match_suggestion_rejections WHERE created_at <= datetime('now', '-90 days')" "DELETE FROM match_suggestion_rejections WHERE rejected_at <= datetime('now', '-90 days')"
).run(); ).run();
tasks.suggestion_rejections = { pruned: changes }; tasks.suggestion_rejections = { pruned: changes };
} catch { tasks.suggestion_rejections = { pruned: 0 }; } } catch { tasks.suggestion_rejections = { pruned: 0 }; }

View File

@ -226,23 +226,13 @@ function loadBills(db, userId) {
} }
function loadRejections(db, userId) { function loadRejections(db, userId) {
try { const rows = db.prepare(`
const rows = db.prepare(` SELECT transaction_id, bill_id
SELECT transaction_id, bill_id FROM match_suggestion_rejections
FROM match_suggestion_rejections WHERE user_id = ?
WHERE user_id = ? AND rejected_at > datetime('now', '-90 days')
AND created_at > datetime('now', '-90 days') `).all(userId);
`).all(userId); return new Set(rows.map(row => suggestionId(row.transaction_id, row.bill_id)));
return new Set(rows.map(row => suggestionId(row.transaction_id, row.bill_id)));
} catch {
// Fall back to all rejections if created_at column doesn't exist yet
try {
const rows = db.prepare(`
SELECT transaction_id, bill_id FROM match_suggestion_rejections WHERE user_id = ?
`).all(userId);
return new Set(rows.map(row => suggestionId(row.transaction_id, row.bill_id)));
} catch { return new Set(); }
}
} }
function loadPriorMatchKeys(db, userId) { function loadPriorMatchKeys(db, userId) {