diff --git a/client/components/BillModal.jsx b/client/components/BillModal.jsx index 27ef6c2..689df6a 100644 --- a/client/components/BillModal.jsx +++ b/client/components/BillModal.jsx @@ -1043,11 +1043,44 @@ export default function BillModal({ bill, initialBill, categories, onClose, onSa {/* Bank Matching Rules */} {!isNew && (
-
-

Bank matching rules

-

- Transactions whose description contains these patterns are automatically imported as payments. -

+
+
+

Bank matching rules

+

+ Transactions whose description contains these patterns are automatically imported as payments. +

+
+ {sourceBill?.has_merchant_rule && ( + + )}
0) { + // Only update balance and mark matched if the payment was actually inserted + if (balCalc) updateBalance.run(balCalc.new_balance, rule.bill_id); + updateTx.run(rule.bill_id, tx.id, userId); + matched++; + } } })(); @@ -126,10 +137,12 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) { if (txRows.length === 0) return { added: 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) - VALUES (?, ?, ?, 'auto_match', ?) + INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id, balance_delta) + VALUES (?, ?, ?, 'provider_sync', ?, ?) `); + const updateBalance = db.prepare("UPDATE bills SET current_balance = ?, updated_at = datetime('now') WHERE id = ?"); const updateTx = db.prepare(` UPDATE transactions SET matched_bill_id = ?, match_status = 'matched', updated_at = datetime('now') @@ -145,10 +158,16 @@ function syncBillPaymentsFromSimplefin(db, userId, billId) { 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++; + const amount = Math.round(Math.abs(tx.amount)) / 100; + const bill = getBill.get(billId); + const balCalc = bill ? computeBalanceDelta(bill, amount) : null; + + const result = insertPayment.run(billId, amount, paidDate, tx.id, balCalc?.balance_delta ?? null); + if (result.changes > 0) { + if (balCalc) updateBalance.run(balCalc.new_balance, billId); + updateTx.run(billId, tx.id, userId); + added++; + } } })(); diff --git a/services/encryptionService.js b/services/encryptionService.js index ee45355..a8b75ab 100644 --- a/services/encryptionService.js +++ b/services/encryptionService.js @@ -10,26 +10,11 @@ const HKDF_INFO = 'bill-tracker-token-encryption-v1'; // Prefix that identifies ciphertext produced with HKDF key derivation. const V2_PREFIX = 'v2:'; -let _warnedAutoKey = false; - -// Returns the raw key material (IKM) without derivation — shared by both paths. +// Returns the raw key material (IKM) without derivation. +// The encryption key is auto-generated on first run and stored in the database +// under `_auto_encryption_key`. No environment variable required — all settings +// live in the app. The key is created once and reused across restarts. function getIkm() { - const envRaw = process.env.TOKEN_ENCRYPTION_KEY || ''; - if (envRaw) { - const buf = Buffer.from(envRaw, 'utf8'); - if (buf.length < 32) throw new Error('TOKEN_ENCRYPTION_KEY must be at least 32 bytes'); - return buf; - } - - if (!_warnedAutoKey) { - _warnedAutoKey = true; - console.warn( - '[security] TOKEN_ENCRYPTION_KEY is not set. Using an auto-generated key ' + - 'stored in the database alongside the encrypted data. Set TOKEN_ENCRYPTION_KEY ' + - 'as an environment variable to keep the key separate from the data it protects.', - ); - } - // Lazy-require to avoid circular dependency at module load time const { getSetting, setSetting } = require('../db/database'); let stored = getSetting('_auto_encryption_key');