fix: historical import backend routes and service adjustments

This commit is contained in:
null 2026-06-04 02:20:30 -05:00
parent d2d3045afe
commit 19421c06fc
2 changed files with 18 additions and 6 deletions

View File

@ -13,7 +13,7 @@ const {
const { amortizationSchedule, debtAprSnapshot } = require('../services/aprService'); const { amortizationSchedule, debtAprSnapshot } = require('../services/aprService');
const { standardizeError } = require('../middleware/errorFormatter'); const { standardizeError } = require('../middleware/errorFormatter');
const { validatePaymentInput } = require('../services/paymentValidation'); const { validatePaymentInput } = require('../services/paymentValidation');
const { addMerchantRule, syncBillPaymentsFromSimplefin } = require('../services/billMerchantRuleService'); const { addMerchantRule, syncBillPaymentsFromSimplefin, merchantMatches } = require('../services/billMerchantRuleService');
const { normalizeMerchant } = require('../services/subscriptionService'); const { normalizeMerchant } = require('../services/subscriptionService');
const { decorateTransaction } = require('../services/transactionService'); const { decorateTransaction } = require('../services/transactionService');
@ -914,7 +914,7 @@ function previewMatchCount(db, userId, normalized) {
`).all(userId); `).all(userId);
return txRows.filter(tx => { return txRows.filter(tx => {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || ''); const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
return txMerchant && (txMerchant.includes(normalized) || normalized.includes(txMerchant)); return txMerchant && merchantMatches(txMerchant, normalized);
}).length; }).length;
} }
@ -1071,7 +1071,7 @@ router.get('/:id/merchant-rules/candidates', (req, res) => {
for (const tx of txRows) { for (const tx of txRows) {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || ''); const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
if (!txMerchant) continue; if (!txMerchant) continue;
const matches = rules.some(r => txMerchant.includes(r) || r.includes(txMerchant)); const matches = rules.some(r => merchantMatches(txMerchant, r));
if (!matches) continue; if (!matches) continue;
const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null); const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);

View File

@ -3,6 +3,18 @@
const { normalizeMerchant } = require('./subscriptionService'); const { normalizeMerchant } = require('./subscriptionService');
const { computeBalanceDelta } = require('./billsService'); const { computeBalanceDelta } = require('./billsService');
// 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.
// Prevents "suno" matching "sunoco", "prime" matching "prime video", etc.
function merchantMatches(txMerchant, ruleMerchant) {
if (!txMerchant || !ruleMerchant) return false;
if (txMerchant === ruleMerchant) return true;
const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const wordBoundary = s => new RegExp(`(^|\\s)${esc(s)}(\\s|$)`);
return wordBoundary(ruleMerchant).test(txMerchant) ||
wordBoundary(txMerchant).test(ruleMerchant);
}
// 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);
@ -90,7 +102,7 @@ function applyMerchantRules(db, userId) {
if (!txMerchant) continue; if (!txMerchant) continue;
const rule = rules.find(r => const rule = rules.find(r =>
txMerchant.includes(r.merchant) || r.merchant.includes(txMerchant) merchantMatches(txMerchant, r.merchant)
); );
if (!rule) continue; if (!rule) continue;
@ -210,7 +222,7 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
for (const tx of txRows) { for (const tx of txRows) {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || ''); const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
if (!txMerchant) continue; if (!txMerchant) continue;
const matches = rules.some(r => txMerchant.includes(r) || r.includes(txMerchant)); const matches = rules.some(r => merchantMatches(txMerchant, r));
if (!matches) continue; if (!matches) continue;
const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null); const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);
if (!paidDate) continue; if (!paidDate) continue;
@ -249,4 +261,4 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) {
return { added, late_attributions: lateAttributions }; return { added, late_attributions: lateAttributions };
} }
module.exports = { addMerchantRule, applyMerchantRules, syncBillPaymentsFromSimplefin }; module.exports = { addMerchantRule, applyMerchantRules, syncBillPaymentsFromSimplefin, merchantMatches };