mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 17:47:00 -05:00
Adds balance variable to rule templates (#5925)
* Adds balance variable to rule templates Enables access to the account balance within rule templates. This allows for more complex rule creation based on the current account balance. Calculates the account balance up to the transaction being processed, including transactions on the same date with lower sort order. Handles cases where the balance is undefined gracefully, defaulting to 0 to prevent errors. ## AI disclaimer This PR contains code that was partially or fully generated by AI and may contain errors. All suggestions for improvement are welcome. * Add release notes * indexed sql params not supported * [autofix.ci] apply automated fixes * Skip parent transactions of splits * Uses aql for account balance Updates transaction rule preparation to use aql instead of sql calculating it from past transactions. The balance is defaulted to 0 if no account is set. Refactor account balance calculation to build a proper query with date and sort_order filters * Add block scoping to switch cases and ensure correct fallthrough handling in Action type conversions * Corrects transaction rule sorting order Reverses the sort order comparison in transaction rules to ensure correct identification of prior transactions with the same date. This ensures that the correct balance is used when calculating balance-based rule conditions. fixup! Corrects transaction rule sorting order * Improves transaction rule balance calculation Uses a more efficient query for calculating the account balance up to a transaction when applying rules, improving performance. This change reduces the complexity of the balance calculation. * Apply coderabbit lessons learned * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -83,10 +83,13 @@ export class Action {
|
||||
|
||||
// Handlebars always returns a string, so we need to convert
|
||||
switch (this.type) {
|
||||
case 'number':
|
||||
object[this.field] = parseFloat(object[this.field]);
|
||||
case 'number': {
|
||||
const numValue = parseFloat(object[this.field]);
|
||||
// If the result is NaN, default to 0 to avoid database insertion errors
|
||||
object[this.field] = isNaN(numValue) ? 0 : numValue;
|
||||
break;
|
||||
case 'date':
|
||||
}
|
||||
case 'date': {
|
||||
const parsed = parseDate(object[this.field]);
|
||||
if (parsed && dateFns.isValid(parsed)) {
|
||||
object[this.field] = format(parsed, 'yyyy-MM-dd');
|
||||
@@ -100,6 +103,7 @@ export class Action {
|
||||
object[this.field] = '9999-12-31';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
object[this.field] = object[this.field] === 'true';
|
||||
break;
|
||||
|
||||
@@ -330,6 +330,15 @@ describe('Action', () => {
|
||||
expect(item.notes).toBe('Hey Sarah! You just payed 10');
|
||||
});
|
||||
|
||||
test('should create actions with balance math operations', () => {
|
||||
// This test ensures the template validation doesn't fail when using balance
|
||||
expect(() => {
|
||||
new Action('set', 'notes', '', {
|
||||
template: '{{ floor (div (mul balance (div 7.99 100)) 12) }}',
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('should not escape text', () => {
|
||||
const action = new Action('set', 'notes', '', {
|
||||
template: '{{notes}}',
|
||||
@@ -467,6 +476,44 @@ describe('Action', () => {
|
||||
testHelper('{{concat "Sarah" "Trops" 12 "Wow"}}', 'SarahTrops12Wow');
|
||||
});
|
||||
|
||||
test('should have access to balance variable', () => {
|
||||
const action = new Action('set', 'notes', '', {
|
||||
template: 'Balance: {{balance}}, Amount: {{amount}}',
|
||||
});
|
||||
const item = { notes: '', amount: 5000, balance: 100000 };
|
||||
action.exec(item);
|
||||
expect(item.notes).toBe('Balance: 100000, Amount: 5000');
|
||||
});
|
||||
|
||||
test('should allow math operations on balance', () => {
|
||||
const action = new Action('set', 'notes', '', {
|
||||
template: 'New balance: {{add balance amount}}',
|
||||
});
|
||||
const item = { notes: '', amount: 5000, balance: 100000 };
|
||||
action.exec(item);
|
||||
expect(item.notes).toBe('New balance: 105000');
|
||||
});
|
||||
|
||||
test('should handle undefined balance gracefully in number fields', () => {
|
||||
const action = new Action('set', 'amount', '', {
|
||||
template: '{{ floor (div (mul balance (div 7.99 100)) 12) }}',
|
||||
});
|
||||
const item = { amount: 0 }; // No balance field
|
||||
action.exec(item);
|
||||
// Should default to 0 instead of NaN when balance is undefined
|
||||
expect(item.amount).toBe(0);
|
||||
});
|
||||
|
||||
test('should calculate with balance in number fields', () => {
|
||||
const action = new Action('set', 'amount', '', {
|
||||
template: '{{ floor (div (mul balance (div 7.99 100)) 12) }}',
|
||||
});
|
||||
const item = { amount: 0, balance: 1200 };
|
||||
action.exec(item);
|
||||
// (1200 * 7.99) / 12 = 7.99 -> floor = 7
|
||||
expect(item.amount).toBe(7);
|
||||
});
|
||||
|
||||
test('{{debug}} should log the item', () => {
|
||||
const action = new Action('set', 'notes', '', {
|
||||
template: '{{debug notes}}',
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
parseDate,
|
||||
dayFromDate,
|
||||
} from '../../shared/months';
|
||||
import { q } from '../../shared/query';
|
||||
import { sortNumbers, getApproxNumberThreshold } from '../../shared/rules';
|
||||
import { ungroupTransaction } from '../../shared/transactions';
|
||||
import { partitionByField, fastSetMerge } from '../../shared/util';
|
||||
@@ -16,7 +17,7 @@ import {
|
||||
type RuleActionEntity,
|
||||
type RuleEntity,
|
||||
} from '../../types/models';
|
||||
import { schemaConfig } from '../aql';
|
||||
import { aqlQuery, schemaConfig } from '../aql';
|
||||
import * as db from '../db';
|
||||
import { getPayee, getPayeeByName, insertPayee, getAccount } from '../db';
|
||||
import { getMappings } from '../db/mappings';
|
||||
@@ -925,6 +926,7 @@ export async function updateCategoryRules(transactions) {
|
||||
export type TransactionForRules = TransactionEntity & {
|
||||
payee_name?: string;
|
||||
_account?: db.DbAccount;
|
||||
balance?: number;
|
||||
};
|
||||
|
||||
export async function prepareTransactionForRules(
|
||||
@@ -939,12 +941,51 @@ export async function prepareTransactionForRules(
|
||||
}
|
||||
}
|
||||
|
||||
r.balance = 0;
|
||||
|
||||
if (trans.account) {
|
||||
if (accounts !== null && accounts.has(trans.account)) {
|
||||
r._account = accounts.get(trans.account);
|
||||
} else {
|
||||
r._account = await getAccount(trans.account);
|
||||
}
|
||||
|
||||
const dateBoundary = trans.date ?? currentDay();
|
||||
let query = q('transactions')
|
||||
.filter({ account: trans.account, is_parent: false })
|
||||
.options({ splits: 'inline' });
|
||||
|
||||
if (trans.id) {
|
||||
query = query.filter({ id: { $ne: trans.id } });
|
||||
}
|
||||
|
||||
const sameDayFilter =
|
||||
trans.sort_order != null
|
||||
? {
|
||||
$and: [
|
||||
{ date: dateBoundary },
|
||||
{ sort_order: { $lt: trans.sort_order } },
|
||||
],
|
||||
}
|
||||
: {
|
||||
$and: [
|
||||
{ date: dateBoundary },
|
||||
{
|
||||
$or: [
|
||||
{ sort_order: { $ne: null } }, // ordered items come before null sort_order
|
||||
...(trans.id ? [{ id: { $lt: trans.id } }] : []), // among nulls, tie-break by id
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { data: balance } = await aqlQuery(
|
||||
query
|
||||
.filter({ $or: [{ date: { $lt: dateBoundary } }, sameDayFilter] })
|
||||
.calculate({ $sum: '$amount' }),
|
||||
);
|
||||
|
||||
r.balance = balance ?? 0;
|
||||
}
|
||||
|
||||
return r;
|
||||
@@ -970,5 +1011,9 @@ export async function finalizeTransactionForRules(
|
||||
delete trans.payee_name;
|
||||
}
|
||||
|
||||
if ('balance' in trans) {
|
||||
delete trans.balance;
|
||||
}
|
||||
|
||||
return trans;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/5925.md
Normal file
6
upcoming-release-notes/5925.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [StephenBrown2]
|
||||
---
|
||||
|
||||
Enables access to the account balance within rule templates.
|
||||
Reference in New Issue
Block a user