- 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');