fix(transactions): Decrypt account names for automation rule evaluation

The rule engine now properly decrypts account names before evaluating automation rules, ensuring accurate matching against encrypted account data. Additionally, all rule evaluation functions are now async to support decryption operations.
This commit is contained in:
Víctor Falcón 2025-12-04 17:02:06 +01:00
parent f6f1cba041
commit 323b7386c1
5 changed files with 72 additions and 36 deletions

View File

@ -163,7 +163,19 @@ export function EditTransactionDialog({
};
}
const result = evaluateRulesForNewTransaction(
const keyString = getStoredKey();
if (!keyString) {
return {
categoryId: null,
notes: null,
notesIv: null,
ruleName: null,
};
}
const key = await importKey(keyString);
const result = await evaluateRulesForNewTransaction(
{
description: description.trim(),
amount: amount / 100,
@ -175,6 +187,7 @@ export function EditTransactionDialog({
categories,
accounts,
banks,
key,
);
if (!result) {
@ -190,20 +203,16 @@ export function EditTransactionDialog({
const finalNotesIv = null;
if (result.note && result.noteIv) {
const keyString = getStoredKey();
if (keyString) {
const key = await importKey(keyString);
const decryptedRuleNote = await decrypt(
result.note,
key,
result.noteIv,
);
const decryptedRuleNote = await decrypt(
result.note,
key,
result.noteIv,
);
finalNotes = appendNoteIfNotPresent(
finalNotes || undefined,
decryptedRuleNote,
);
}
finalNotes = appendNoteIfNotPresent(
finalNotes || undefined,
decryptedRuleNote,
);
}
return {

View File

@ -351,7 +351,7 @@ export function ImportTransactionsDrawer({
let notesIv: string | null = null;
if (key && rules.length > 0) {
const ruleMatch = evaluateRulesForNewTransaction(
const ruleMatch = await evaluateRulesForNewTransaction(
{
description: transaction.description,
amount: transaction.amount / 100,
@ -362,6 +362,7 @@ export function ImportTransactionsDrawer({
freshCategories,
freshAccounts,
freshBanks,
key,
);
if (ruleMatch) {

View File

@ -65,12 +65,13 @@ export function useReEvaluateAllTransactions() {
description: transaction.decryptedDescription,
});
const result = evaluateRules(
const result = await evaluateRules(
transaction,
rules,
categories,
accounts,
banks,
key,
);
if (result) {

View File

@ -1,3 +1,4 @@
import { decrypt } from '@/lib/crypto';
import { consoleDebug } from '@/lib/debug';
import type { Account, Bank } from '@/types/account';
import type { AutomationRule } from '@/types/automation-rule';
@ -56,12 +57,29 @@ function normalizeRuleJson(rulesJson: unknown): unknown {
return rulesJson;
}
export function prepareTransactionData(
async function decryptAccountName(
account: Account,
key: CryptoKey,
): Promise<string> {
try {
return await decrypt(account.name, key, account.name_iv);
} catch (error) {
console.error('Failed to decrypt account name:', account.id, error);
return '';
}
}
const normalizeWhitespace = (str: string): string => {
return str.trim().replace(/\s+/g, ' ');
};
export async function prepareTransactionData(
transaction: DecryptedTransaction,
accounts: Account[],
banks: Bank[],
categories: Category[],
): TransactionData {
encryptionKey: CryptoKey,
): Promise<TransactionData> {
const account = accounts.find((a) => a.id === transaction.account_id);
const bank = account?.bank?.id
? banks.find((b) => b.id === account.bank.id)
@ -70,9 +88,9 @@ export function prepareTransactionData(
? categories.find((c) => c.id === transaction.category_id)
: null;
const normalizeWhitespace = (str: string): string => {
return str.trim().replace(/\s+/g, ' ');
};
const accountName = account
? await decryptAccountName(account, encryptionKey)
: '';
return {
description: normalizeWhitespace(
@ -81,7 +99,7 @@ export function prepareTransactionData(
amount: transaction.amount / 100,
transaction_date: transaction.transaction_date,
bank_name: bank?.name || '',
account_name: account?.name || '',
account_name: accountName.toLowerCase(),
category: category?.name || null,
notes: transaction.decryptedNotes
? normalizeWhitespace(transaction.decryptedNotes.toLowerCase())
@ -89,20 +107,22 @@ export function prepareTransactionData(
};
}
export function evaluateRules(
export async function evaluateRules(
transaction: DecryptedTransaction,
rules: AutomationRule[],
categories: Category[],
accounts: Account[],
banks: Bank[],
): RuleEvaluationResult | null {
encryptionKey: CryptoKey,
): Promise<RuleEvaluationResult | null> {
const sortedRules = [...rules].sort((a, b) => a.priority - b.priority);
const transactionData = prepareTransactionData(
const transactionData = await prepareTransactionData(
transaction,
accounts,
banks,
categories,
encryptionKey,
);
consoleDebug('[Rule Engine] Transaction data prepared:', transactionData);
@ -150,22 +170,24 @@ export function evaluateRules(
return null;
}
export function evaluateRulesForTransactions(
export async function evaluateRulesForTransactions(
transactions: DecryptedTransaction[],
rules: AutomationRule[],
categories: Category[],
accounts: Account[],
banks: Bank[],
): Map<string, RuleEvaluationResult> {
encryptionKey: CryptoKey,
): Promise<Map<string, RuleEvaluationResult>> {
const results = new Map<string, RuleEvaluationResult>();
for (const transaction of transactions) {
const result = evaluateRules(
const result = await evaluateRules(
transaction,
rules,
categories,
accounts,
banks,
encryptionKey,
);
if (result) {
@ -184,13 +206,14 @@ export interface NewTransactionData {
notes?: string;
}
export function evaluateRulesForNewTransaction(
export async function evaluateRulesForNewTransaction(
transactionData: NewTransactionData,
rules: AutomationRule[],
categories: Category[],
accounts: Account[],
banks: Bank[],
): RuleEvaluationResult | null {
encryptionKey: CryptoKey,
): Promise<RuleEvaluationResult | null> {
if (!rules || !categories || !accounts || !banks) {
consoleDebug(
'[Rule Engine] Missing required data for rule evaluation',
@ -215,9 +238,9 @@ export function evaluateRulesForNewTransaction(
? banks.find((b) => b.id === account.bank.id)
: undefined;
const normalizeWhitespace = (str: string): string => {
return str.trim().replace(/\s+/g, ' ');
};
const accountName = account
? await decryptAccountName(account, encryptionKey)
: '';
const preparedData: TransactionData = {
description: normalizeWhitespace(
@ -226,7 +249,7 @@ export function evaluateRulesForNewTransaction(
amount: transactionData.amount,
transaction_date: transactionData.transaction_date,
bank_name: bank?.name || '',
account_name: account?.name || '',
account_name: accountName.toLowerCase(),
category: null,
notes: transactionData.notes
? normalizeWhitespace(transactionData.notes.toLowerCase())

View File

@ -584,12 +584,13 @@ export default function Transactions({ categories, accounts, banks }: Props) {
}
consoleDebug('Evaluating rules against transaction...');
const result = evaluateRules(
const result = await evaluateRules(
transaction,
rules,
categories,
accounts,
banks,
key,
);
consoleDebug('Rule evaluation result:', result);
@ -733,12 +734,13 @@ export default function Transactions({ categories, accounts, banks }: Props) {
for (const transaction of selectedTransactions) {
consoleDebug(`\nEvaluating transaction ${transaction.id}...`);
const result = evaluateRules(
const result = await evaluateRules(
transaction,
rules,
categories,
accounts,
banks,
key,
);
consoleDebug('Rule evaluation result:', result);