BillTracker/services/billMerchantRuleService.js

85 lines
2.6 KiB
JavaScript

'use strict';
const { normalizeMerchant } = require('./subscriptionService');
// Persist a merchant→bill rule so future synced transactions auto-match.
function addMerchantRule(db, userId, billId, merchant) {
const normalized = normalizeMerchant(merchant);
if (!normalized || normalized.length < 3) return;
try {
db.prepare(`
INSERT INTO bill_merchant_rules (user_id, bill_id, merchant)
VALUES (?, ?, ?)
ON CONFLICT(user_id, bill_id, merchant) DO NOTHING
`).run(userId, billId, normalized);
} catch {
// Table may not exist yet on legacy DBs — safe to skip
}
}
// Scan all unmatched negative transactions for this user, apply any stored
// merchant rules, create payments, and mark the transactions matched.
// Returns { matched: number }.
function applyMerchantRules(db, userId) {
let rules;
try {
rules = db.prepare(`
SELECT bmr.bill_id, bmr.merchant
FROM bill_merchant_rules bmr
JOIN bills b ON b.id = bmr.bill_id AND b.user_id = bmr.user_id AND b.deleted_at IS NULL
WHERE bmr.user_id = ?
`).all(userId);
} catch {
return { matched: 0 };
}
if (rules.length === 0) return { matched: 0 };
const txRows = db.prepare(`
SELECT id, amount, payee, description, memo, posted_date, transacted_at
FROM transactions
WHERE user_id = ?
AND match_status = 'unmatched'
AND ignored = 0
AND amount < 0
`).all(userId);
if (txRows.length === 0) return { matched: 0 };
const insertPayment = db.prepare(`
INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id)
VALUES (?, ?, ?, 'auto_match', ?)
`);
const updateTx = db.prepare(`
UPDATE transactions
SET matched_bill_id = ?, match_status = 'matched', updated_at = datetime('now')
WHERE id = ? AND user_id = ? AND match_status = 'unmatched'
`);
let matched = 0;
db.transaction(() => {
for (const tx of txRows) {
const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || '');
if (!txMerchant) continue;
const rule = rules.find(r =>
txMerchant.includes(r.merchant) || r.merchant.includes(txMerchant)
);
if (!rule) continue;
const paidDate = tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);
if (!paidDate) continue;
const amount = Math.round(Math.abs(tx.amount)) / 100;
insertPayment.run(rule.bill_id, amount, paidDate, tx.id);
updateTx.run(rule.bill_id, tx.id, userId);
matched++;
}
})();
return { matched };
}
module.exports = { addMerchantRule, applyMerchantRules };