'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 }; } // Sync all unmatched SimpleFIN transactions for a single bill using its stored // merchant rules. If no rule exists yet but the bill has a detected merchant in // its notes, the rule is created on the fly. // Returns { added: number }. function syncBillPaymentsFromSimplefin(db, userId, billId) { // Load rules for this specific bill let rules; try { rules = db.prepare(` SELECT merchant FROM bill_merchant_rules WHERE user_id = ? AND bill_id = ? `).all(userId, billId).map(r => r.merchant); } catch { return { added: 0 }; } // Fallback: extract merchant from notes "Detected from recurring merchant: X" if (rules.length === 0) { const bill = db.prepare('SELECT notes FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL').get(billId, userId); const match = bill?.notes?.match(/Detected from recurring merchant:\s*(.+)/i); if (match) { const extracted = normalizeMerchant(match[1].trim()); if (extracted && extracted.length >= 3) { addMerchantRule(db, userId, billId, extracted); rules = [extracted]; } } if (rules.length === 0) return { added: 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 { added: 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 added = 0; db.transaction(() => { for (const tx of txRows) { const txMerchant = normalizeMerchant(tx.payee || tx.description || tx.memo || ''); if (!txMerchant) continue; const matches = rules.some(r => txMerchant.includes(r) || r.includes(txMerchant)); if (!matches) 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(billId, amount, paidDate, tx.id); updateTx.run(billId, tx.id, userId); added++; } })(); return { added }; } module.exports = { addMerchantRule, applyMerchantRules, syncBillPaymentsFromSimplefin };