fix: match "is nothing" rule conditions on transactions without a payee or category (#9017)

This commit is contained in:
Mohammed Yasin Zuhayr
2026-10-03 14:05:56 +00:00
committed by GitHub
parent 821caa2643
commit b490e3e76e
3 changed files with 101 additions and 4 deletions
@@ -406,6 +406,74 @@ describe('Transaction rules', () => {
});
});
test('"payee is nothing" matches a transaction without a payee field', async () => {
await loadRules();
await insertRule({
stage: null,
conditionsOp: 'and',
conditions: [{ op: 'is', field: 'payee', value: null }],
actions: [{ op: 'set', field: 'notes', value: 'no payee' }],
});
// Bank sync leaves `payee` out entirely when the import has no payee
expect(
await runRules({
imported_payee: 'kroger',
date: '2020-08-11',
amount: 50,
}),
).toEqual({
imported_payee: 'kroger',
date: '2020-08-11',
amount: 50,
notes: 'no payee',
});
// A payee set by a rule action is kept
await insertRule({
stage: null,
conditionsOp: 'and',
conditions: [{ op: 'is', field: 'payee', value: null }],
actions: [{ op: 'set', field: 'payee', value: 'kroger' }],
});
expect(
await runRules({
imported_payee: 'kroger',
date: '2020-08-11',
amount: 50,
}),
).toEqual({
imported_payee: 'kroger',
date: '2020-08-11',
amount: 50,
payee: 'kroger',
notes: 'no payee',
});
});
test('"category is nothing" matches a transaction without a category field', async () => {
await loadRules();
await insertRule({
stage: null,
conditionsOp: 'and',
conditions: [{ op: 'is', field: 'category', value: null }],
actions: [{ op: 'set', field: 'notes', value: 'no category' }],
});
expect(
await runRules({
payee: 'kroger',
date: '2020-08-11',
amount: 50,
}),
).toEqual({
payee: 'kroger',
date: '2020-08-11',
amount: 50,
notes: 'no category',
});
});
test('category_group condition matches categories in that group (live)', async () => {
await loadRules();
const billsGroupId = await db.insertCategoryGroup({ name: 'Bills' });
@@ -318,6 +318,8 @@ export async function getAllRuleIdsFromSchedules(
}
// Runner
const NULLABLE_RULE_FIELDS = ['payee', 'category'] as const;
export async function runRules(
trans,
accounts: Map<string, db.DbAccount> | null = null,
@@ -340,9 +342,24 @@ export async function runRules(
),
);
let finalTrans = await prepareTransactionForRules({ ...trans }, accountsMap, {
includeBalance: false,
});
// Bank sync and the client can leave `payee` or `category` out entirely
// when the transaction has none. A condition never matches a missing
// field, so a "payee is nothing" rule would silently skip such a
// transaction. Match against null instead, and drop the field again
// afterwards so the returned shape matches the input.
const missingNullableFields = NULLABLE_RULE_FIELDS.filter(
field => trans[field] === undefined,
);
const normalizedTrans = { ...trans };
for (const field of missingNullableFields) {
normalizedTrans[field] = null;
}
let finalTrans = await prepareTransactionForRules(
normalizedTrans,
accountsMap,
{ includeBalance: false },
);
let lastCategoryIdForGroup: string | null = finalTrans.category ?? null;
// The running balance is a query over every earlier transaction in
@@ -431,7 +448,13 @@ export async function runRules(
}
}
return await finalizeTransactionForRules(finalTrans);
const result = await finalizeTransactionForRules(finalTrans);
for (const field of missingNullableFields) {
if (result[field] === null) {
delete result[field];
}
}
return result;
}
function conditionSpecialCases(cond: Condition | null): Condition | null {
@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [YasinzHyper]
---
Fix "payee is nothing" and "category is nothing" rule conditions not matching synced transactions that have no payee or category