Resolve payee name during rule evaluation (#8360)

When a rule action sets payee_name, resolve it to the actual payee ID
immediately during rule execution instead of waiting until finalization.

This allows later staged rules to match on the resolved payee.
Previously, payee_name actions temporarily set payee to "new", and the
real payee ID was only resolved in finalizeTransactionForRules after
rule matching had completed.

Fixes #8359
This commit is contained in:
Julien Vincent
2026-06-29 21:25:12 +00:00
committed by GitHub
parent e891c2e30e
commit d9c69b08ed
3 changed files with 65 additions and 13 deletions
@@ -220,6 +220,42 @@ describe('Transaction rules', () => {
expect(transaction.category).toBe(null);
});
test('payee rules match after a staged formula sets payee name', async () => {
await loadRules();
await db.insertPayee({ id: 'amazon_id', name: 'Amazon' });
await insertRule({
stage: 'pre',
conditionsOp: 'and',
conditions: [{ op: 'is', field: 'imported_payee', value: 'AMZN MKTP' }],
actions: [
{
op: 'set',
field: 'payee_name',
value: null,
options: { formula: '="Amazon"' },
},
],
});
await insertRule({
stage: null,
conditionsOp: 'and',
conditions: [{ op: 'is', field: 'payee', value: 'amazon_id' }],
actions: [{ op: 'set', field: 'category', value: 'shopping' }],
});
const transaction = await runRules({
imported_payee: 'AMZN MKTP',
payee: null,
category: null,
});
expect(transaction.payee).toBe('amazon_id');
expect(transaction.category).toBe('shopping');
});
test('loadRules loads all the rules', async () => {
await loadRules();
await insertRule({
@@ -370,16 +370,19 @@ export async function runRules(
// bypass condition checking to run the rule even if the transaction date falls outside of the schedule's date range.
const changes = rules[i].execActions(finalTrans);
finalTrans = Object.assign({}, finalTrans, changes);
await resolvePayeeNameForRules(finalTrans);
} else if (RuleIdsLinkedToSchedules.includes(rules[i].id)) {
// skip all other rules that are linked to other schedules.
continue;
} else {
// if a rule is not linked to a schedule, run it.
finalTrans = rules[i].apply(finalTrans);
await resolvePayeeNameForRules(finalTrans);
}
} else {
// if there is no scheduleRuleID then just run all rules.
finalTrans = rules[i].apply(finalTrans);
await resolvePayeeNameForRules(finalTrans);
}
}
@@ -1098,23 +1101,30 @@ export async function prepareTransactionForRules(
return r;
}
async function resolvePayeeNameForRules(
trans: TransactionEntity | TransactionForRules,
): Promise<void> {
if (!('payee_name' in trans) || trans.payee !== 'new') {
return;
}
if (trans.payee_name) {
let payee_id = (await getPayeeByName(trans.payee_name))?.id;
payee_id ??= await insertPayee({
name: trans.payee_name,
});
trans.payee = payee_id;
} else {
trans.payee = null;
}
}
export async function finalizeTransactionForRules(
trans: TransactionEntity | TransactionForRules,
): Promise<TransactionEntity> {
if ('payee_name' in trans) {
if (trans.payee === 'new') {
if (trans.payee_name) {
let payeeId = (await getPayeeByName(trans.payee_name))?.id;
payeeId ??= await insertPayee({
name: trans.payee_name,
});
trans.payee = payeeId;
} else {
trans.payee = null;
}
}
await resolvePayeeNameForRules(trans);
delete trans.payee_name;
}
@@ -0,0 +1,6 @@
---
category: Bugfixes
authors: [julienvincent]
---
When a rule action sets payee_name, resolve it to the actual payee ID immediately during rule execution instead of waiting until finalization.