diff --git a/packages/loot-core/src/server/transactions/transaction-rules.test.ts b/packages/loot-core/src/server/transactions/transaction-rules.test.ts index 4fda79077d..ff8d5bbdbe 100644 --- a/packages/loot-core/src/server/transactions/transaction-rules.test.ts +++ b/packages/loot-core/src/server/transactions/transaction-rules.test.ts @@ -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' }); diff --git a/packages/loot-core/src/server/transactions/transaction-rules.ts b/packages/loot-core/src/server/transactions/transaction-rules.ts index a3e3329ad3..26af124d99 100644 --- a/packages/loot-core/src/server/transactions/transaction-rules.ts +++ b/packages/loot-core/src/server/transactions/transaction-rules.ts @@ -318,6 +318,8 @@ export async function getAllRuleIdsFromSchedules( } // Runner +const NULLABLE_RULE_FIELDS = ['payee', 'category'] as const; + export async function runRules( trans, accounts: Map | 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 { diff --git a/upcoming-release-notes/rules-payee-is-nothing.md b/upcoming-release-notes/rules-payee-is-nothing.md new file mode 100644 index 0000000000..33c4a03530 --- /dev/null +++ b/upcoming-release-notes/rules-payee-is-nothing.md @@ -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