actual/packages/loot-core/src/server/accounts/transaction-rules.test.js
Tom French 9c0df36e16
Sort import in alphabetical order (#238)
* style: enforce sorting of imports

* style: alphabetize imports

* style: merge duplicated imports
2022-09-02 15:07:24 +01:00

946 lines
26 KiB
JavaScript

import q from '../../shared/query';
import { runQuery } from '../aql';
import * as db from '../db';
import { loadMappings } from '../db/mappings';
import {
getRules,
loadRules,
insertRule,
updateRule,
deleteRule,
makeRule,
runRules,
conditionsToAQL,
resetState,
getProbableCategory,
updateCategoryRules,
migrateOldRules
} from './transaction-rules';
// TODO: write tests to make sure payee renaming is "pre" and category
// setting is "null" stage
beforeEach(async () => {
await global.emptyDatabase()();
resetState();
await loadMappings();
});
async function getMatchingTransactions(conds) {
let { filters } = conditionsToAQL(conds);
let { data } = await runQuery(
q('transactions')
.filter({ $and: filters })
.select('*')
);
return data;
}
describe('Transaction rules', () => {
test('makeRule validates rule data', () => {
const spy = jest.spyOn(console, 'warn').mockImplementation();
// Parse errors
expect(makeRule({ conditions: '{', actions: '[]' })).toBe(null);
expect(makeRule({ conditions: '[]', actions: '{' })).toBe(null);
expect(makeRule({ conditions: '{}', actions: '{}' })).toBe(null);
// This is valid
expect(makeRule({ conditions: '[]', actions: '[]' })).not.toBe(null);
// condition has invalid operator
expect(
makeRule({
conditions: JSON.stringify([
{ op: 'noop', field: 'date', value: '2019-05' }
]),
actions: JSON.stringify([
{ op: 'set', field: 'name', value: 'Sarah' },
{ op: 'set', field: 'category', value: 'Sarah' }
])
})
).toBe(null);
// setting an invalid field
expect(
makeRule({
conditions: JSON.stringify([
{ op: 'is', field: 'date', value: '2019-05' }
]),
actions: JSON.stringify([
{ op: 'set', field: 'notes', value: 'Sarah' },
{ op: 'set', field: 'invalid', value: 'Sarah' }
])
})
).toBe(null);
// condition has valid operator & setting valid fields
expect(
makeRule({
conditions: JSON.stringify([
{ op: 'is', field: 'date', value: '2019-05' }
]),
actions: JSON.stringify([
{ op: 'set', field: 'notes', value: 'Sarah' },
{ op: 'set', field: 'category', value: 'Sarah' }
])
})
).not.toBe(null);
spy.mockRestore();
});
test('insert a rule into the database', async () => {
await loadRules();
await insertRule({ stage: 'pre', conditions: [], actions: [] });
expect((await db.all('SELECT * FROM rules')).length).toBe(1);
// Make sure it was projected
expect(getRules().length).toBe(1);
await insertRule({
stage: 'pre',
conditions: [{ op: 'is', field: 'date', value: '2019-05' }],
actions: [
{ op: 'set', field: 'notes', value: 'Sarah' },
{ op: 'set', field: 'category', value: 'food' }
]
});
expect((await db.all('SELECT * FROM rules')).length).toBe(2);
expect(getRules().length).toBe(2);
const spy = jest.spyOn(console, 'warn').mockImplementation();
// Try to insert an invalid rule (don't use `insertRule` because
// that will validate the input)
await db.insertWithUUID('rules', { conditions: '{', actions: '}' });
// It will be in the database
expect((await db.all('SELECT * FROM rules')).length).toBe(3);
// But it will be ignored
expect(getRules().length).toBe(2);
spy.mockRestore();
// Finally make sure the rule is actually in place and runs
let transaction = runRules({
date: '2019-05-10',
notes: '',
category: null
});
expect(transaction.date).toBe('2019-05-10');
expect(transaction.notes).toBe('Sarah');
expect(transaction.category).toBe('food');
});
test('update a rule in the database', async () => {
await loadRules();
let id = await insertRule({
stage: 'pre',
conditions: [{ op: 'is', field: 'imported_payee', value: 'kroger' }],
actions: [
{ op: 'set', field: 'notes', value: 'Sarah' },
{ op: 'set', field: 'category', value: 'food' }
]
});
expect(getRules().length).toBe(1);
let transaction = runRules({
imported_payee: 'Kroger',
notes: '',
category: null
});
expect(transaction.imported_payee).toBe('Kroger');
expect(transaction.notes).toBe('Sarah');
expect(transaction.category).toBe('food');
// Change the action
await updateRule({
id,
actions: [{ op: 'set', field: 'category', value: 'bars' }]
});
expect(getRules().length).toBe(1);
transaction = runRules({
imported_payee: 'Kroger',
notes: '',
category: null
});
expect(transaction.imported_payee).toBe('Kroger');
expect(transaction.notes).toBe('');
expect(transaction.category).toBe('bars');
// If changing the condition, make sure the rule is re-indexed
await updateRule({
id,
conditions: [{ op: 'is', field: 'imported_payee', value: 'ABC' }]
});
transaction = runRules({
imported_payee: 'ABC',
notes: '',
category: null
});
expect(transaction.category).toBe('bars');
expect(getRules().length).toBe(1);
});
test('delete a rule in the database', async () => {
await loadRules();
let id = await insertRule({
stage: 'pre',
conditions: [{ op: 'is', field: 'payee', value: 'kroger' }],
actions: [
{ op: 'set', field: 'notes', value: 'Sarah' },
{ op: 'set', field: 'category', value: 'food' }
]
});
expect(getRules().length).toBe(1);
let transaction = runRules({
payee: 'Kroger',
notes: '',
category: null
});
expect(transaction.payee).toBe('Kroger');
expect(transaction.category).toBe('food');
await deleteRule({ id });
expect(getRules().length).toBe(0);
transaction = runRules({
payee: 'Kroger',
notes: '',
category: null
});
expect(transaction.payee).toBe('Kroger');
expect(transaction.category).toBe(null);
});
test('loadRules loads all the rules', async () => {
await loadRules();
await insertRule({
stage: 'pre',
conditions: [{ op: 'contains', field: 'imported_payee', value: 'lowes' }],
actions: [{ op: 'set', field: 'payee', value: 'lowes' }]
});
await insertRule({
stage: 'post',
conditions: [{ op: 'is', field: 'imported_payee', value: 'kroger' }],
actions: [{ op: 'set', field: 'notes', value: 'Sarah' }]
});
resetState();
expect(getRules().length).toBe(0);
await loadRules();
expect(getRules().length).toBe(2);
let transaction = runRules({
imported_payee: 'blah Lowes blah',
payee: null,
category: null
});
expect(transaction.payee).toBe('lowes');
transaction = runRules({
imported_payee: 'kroger',
category: null
});
expect(transaction.notes).toBe('Sarah');
});
test('ids in rules are migrated as mapping changes', async () => {
await loadRules();
await db.insertPayee({ id: 'home_id', name: 'home' });
await db.insertPayee({ id: 'lowes_id', name: 'lowes' });
await db.insertCategoryGroup({ name: 'group' });
await db.insertCategory({
id: 'food_id',
name: 'food',
cat_group: 'group'
});
await db.insertCategory({
id: 'beer_id',
name: 'beer',
cat_group: 'group'
});
await insertRule({
id: 'one',
stage: 'pre',
conditions: [{ op: 'contains', field: 'imported_payee', value: 'lowes' }],
actions: [{ op: 'set', field: 'payee', value: 'lowes_id' }]
});
await insertRule({
id: 'two',
stage: 'pre',
conditions: [
{ op: 'is', field: 'payee', value: 'lowes_id' },
{ op: 'is', field: 'category', value: 'food_id' }
],
actions: [{ op: 'set', field: 'notes', value: 'Sarah' }]
});
let rule1 = getRules().find(r => r.id === 'one');
let rule2 = getRules().find(r => r.id === 'two');
expect(rule1.actions[0].value).toBe('lowes_id');
expect(rule2.conditions[0].value).toBe('lowes_id');
await db.mergePayees('home_id', ['lowes_id']);
expect(rule1.actions[0].value).toBe('home_id');
expect(rule2.conditions[0].value).toBe('home_id');
expect(rule2.conditions[1].value).toBe('food_id');
await db.deleteCategory({ id: 'food_id' }, 'beer_id');
expect(rule2.conditions[1].value).toBe('beer_id');
await loadRules();
// Make sure mappings work when loading fresh
rule1 = getRules().find(r => r.id === 'one');
rule2 = getRules().find(r => r.id === 'two');
expect(rule1.actions[0].value).toBe('home_id');
expect(rule2.conditions[0].value).toBe('home_id');
expect(rule2.conditions[1].value).toBe('beer_id');
});
test('runRules runs all the rules in each phase', async () => {
await loadRules();
await insertRule({
stage: 'post',
conditions: [
{
op: 'oneOf',
field: 'payee',
value: ['kroger', 'kroger1', 'kroger2', 'kroger3', 'kroger4']
}
],
actions: [{ op: 'set', field: 'notes', value: 'got it2' }]
});
await insertRule({
stage: 'pre',
conditions: [{ op: 'is', field: 'imported_payee', value: '123 kroger' }],
actions: [{ op: 'set', field: 'payee', value: 'kroger3' }]
});
await insertRule({
stage: null,
conditions: [
{ op: 'contains', field: 'imported_payee', value: 'kroger' }
],
actions: [{ op: 'set', field: 'payee', value: 'kroger4' }]
});
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'kroger4' }],
actions: [{ op: 'set', field: 'notes', value: 'got it' }]
});
expect(
runRules({
imported_payee: '123 kroger',
date: '2020-08-11',
amount: 50
})
).toEqual({
date: '2020-08-11',
imported_payee: '123 kroger',
payee: 'kroger4',
amount: 50,
notes: 'got it2'
});
});
test('migrating from the old payee rules works', async () => {
await loadRules();
let categoryGroupId = await db.insertCategoryGroup({ name: 'general' });
let categoryId = await db.insertCategory({
name: 'food',
cat_group: categoryGroupId
});
let krogerId = await db.insertPayee({ name: 'kroger' });
let lowesId = await db.insertPayee({ name: 'lowes', category: categoryId });
await db.insertPayeeRule({
payee_id: krogerId,
type: 'contains',
value: 'kroger'
});
await db.insertPayeeRule({
payee_id: lowesId,
type: 'equals',
value: '123 lowes'
});
await db.insertPayeeRule({
payee_id: lowesId,
type: 'equals',
value: 'lowes 456'
});
// Migrate!
await migrateOldRules();
expect(getRules().length).toBe(3);
expect(runRules({ payee: null, imported_payee: '123 lowes' })).toEqual({
category: categoryId,
imported_payee: '123 lowes',
payee: lowesId
});
expect(runRules({ payee: null, imported_payee: 'lowes 456' })).toEqual({
category: categoryId,
imported_payee: 'lowes 456',
payee: lowesId
});
expect(runRules({ payee: null, imported_payee: '1 lowes 2' })).toEqual({
imported_payee: '1 lowes 2',
payee: null
});
expect(
runRules({
payee: null,
imported_payee: 'blah blah kroger bla',
category: null
})
).toEqual({
imported_payee: 'blah blah kroger bla',
payee: krogerId,
category: null
});
});
test('transactions can be queried by rule', async () => {
await loadRules();
let account = await db.insertAccount({ name: 'bank' });
let categoryGroupId = await db.insertCategoryGroup({ name: 'general' });
let categoryId = await db.insertCategory({
name: 'food',
cat_group: categoryGroupId
});
let krogerId = await db.insertPayee({ name: 'kroger' });
let lowesId = await db.insertPayee({ name: 'lowes', category: categoryId });
await db.insertTransaction({
id: '1',
date: '2020-10-01',
account,
payee: krogerId,
notes: 'barr',
amount: 353
});
await db.insertTransaction({
id: '2',
date: '2020-10-15',
account,
payee: krogerId,
notes: 'fooo',
amount: 453
});
await db.insertTransaction({
id: '3',
date: '2020-10-15',
account,
payee: lowesId,
notes: 'FooO',
amount: -322
});
let transactions = await getMatchingTransactions([
{ field: 'date', op: 'is', value: '2020-10-15' }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '3']);
transactions = await getMatchingTransactions([
{ field: 'payee', op: 'is', value: lowesId }
]);
expect(transactions.map(t => t.id)).toEqual(['3']);
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'is', value: 353 }
]);
expect(transactions.map(t => t.id)).toEqual(['1']);
transactions = await getMatchingTransactions([
{ field: 'notes', op: 'is', value: 'FooO' }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '3']);
transactions = await getMatchingTransactions([
{ field: 'notes', op: 'contains', value: 'oo' }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '3']);
transactions = await getMatchingTransactions([
{ field: 'notes', op: 'oneOf', value: ['fooo', 'barr'] }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '3', '1']);
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'gt', value: 300 }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '1']);
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'gt', value: 400 },
{ field: 'amount', op: 'lt', value: 500 }
]);
expect(transactions.map(t => t.id)).toEqual(['2']);
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'gt', value: 300, options: { inflow: true } },
{ field: 'amount', op: 'lt', value: 400, options: { inflow: true } }
]);
expect(transactions.map(t => t.id)).toEqual(['1']);
// If `inflow` is true, it should never return outflow transactions
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'gt', value: -1000, options: { inflow: true } }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '1']);
// Same thing for `outflow`: never return `inflow` transactions
transactions = await getMatchingTransactions([
{ field: 'amount', op: 'gt', value: 300, options: { outflow: true } }
]);
expect(transactions.map(t => t.id)).toEqual(['3']);
transactions = await getMatchingTransactions([
{ field: 'date', op: 'gt', value: '2020-10-10' }
]);
expect(transactions.map(t => t.id)).toEqual(['2', '3']);
// todo: isapprox
});
});
describe('Learning categories', () => {
function expectCategoryRule(rule, category, expectedPayee) {
expect(rule.conditions.length).toBe(1);
expect(rule.conditions[0].op).toBe('is');
expect(rule.conditions[0].field).toBe('payee');
expect(rule.conditions[0].value).toBe(expectedPayee);
expect(rule.actions.length).toBe(1);
expect(rule.actions[0].op).toBe('set');
expect(rule.actions[0].field).toBe('category');
expect(rule.actions[0].value).toBe(category);
}
async function insertTransaction(
transaction,
expectedCategory,
expectedRuleCount = 1,
expectedPayee = 'foo'
) {
await db.insertTransaction(transaction);
await updateCategoryRules([transaction]);
expect(getRules().length).toBe(expectedRuleCount);
if (expectedRuleCount > 0) {
expectCategoryRule(
getRules()[expectedRuleCount - 1],
expectedCategory,
expectedPayee
);
}
}
async function loadData() {
await loadRules();
await db.insertAccount({ id: 'acct', name: 'acct' });
await db.insertCategoryGroup({ id: 'catg', name: 'catg' });
await db.insertCategory({ id: 'food', name: 'food', cat_group: 'catg' });
await db.insertCategory({ id: 'beer', name: 'beer', cat_group: 'catg' });
await db.insertCategory({ id: 'fun', name: 'fun', cat_group: 'catg' });
await db.insertPayee({ id: 'foo', name: 'foo' });
await db.insertPayee({ id: 'bar', name: 'bar' });
}
test('getProbableCategory estimates a category winner', () => {
let winner = getProbableCategory([{ category: 'foo' }]);
// It needs at least 3 transactions
expect(winner).toBe(null);
winner = getProbableCategory([
{ category: 'foo' },
{ category: 'foo' },
{ category: 'foo' }
]);
expect(winner).toBe('foo');
winner = getProbableCategory([
{ category: 'bar' },
{ category: 'foo' },
{ category: 'foo' },
{ category: 'foo' }
]);
expect(winner).toBe('foo');
winner = getProbableCategory([
{ category: 'bar' },
{ category: 'bar' },
{ category: 'bar' },
{ category: 'foo' },
{ category: 'foo' },
{ category: 'foo' }
]);
expect(winner).toBe('bar');
});
test('creates rule when inserting transactions', async () => {
await loadData();
await insertTransaction(
{
id: 'one',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
null,
0
);
await insertTransaction(
{
id: 'two',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
null,
0
);
await insertTransaction(
{
id: 'three',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
'food'
);
});
test('leaves existing rule alone if probable category is ambiguous', async () => {
await loadData();
await insertTransaction(
{
id: 'one',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
null,
0
);
await insertTransaction(
{
id: 'two',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'beer'
},
null,
0
);
await insertTransaction(
{
id: 'three',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'beer'
},
null,
0
);
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'fun' }]
});
// Even though the system couldn't figure out the category to set,
// it should leave the existing rule alone
await insertTransaction(
{
id: 'four',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'bills'
},
'fun',
1
);
});
test('updates an existing rule', async () => {
await loadData();
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'beer' }]
});
await insertTransaction(
{
id: 'one',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
'beer',
1
);
await insertTransaction(
{
id: 'two',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
'beer',
1
);
await insertTransaction(
{
id: 'three',
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
},
'food',
1
);
});
test('works with multiple payees', async () => {
await loadData();
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'beer' }]
});
// Use a new payee, so the category should be remembered
await insertTransaction(
{
id: 'three',
date: '2016-12-03',
account: 'acct',
payee: 'bar',
category: 'fun'
},
'beer',
1
);
await insertTransaction(
{
id: 'four',
date: '2016-12-03',
account: 'acct',
payee: 'bar',
category: 'fun'
},
'beer',
1
);
await insertTransaction(
{
id: 'five',
date: '2016-12-03',
account: 'acct',
payee: 'bar',
category: 'fun'
},
'fun',
2,
'bar'
);
});
test('updates rules correctly even if multiple rules exist', async () => {
await loadData();
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'unknown1' }]
});
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'unknown2' }]
});
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: null }],
actions: [{ op: 'set', field: 'category', value: 'beer' }]
});
let trans = {
date: '2016-12-01',
account: 'acct',
payee: 'foo',
category: 'food'
};
await db.insertTransaction({ ...trans, id: 'one' });
await db.insertTransaction({ ...trans, id: 'two' });
await db.insertTransaction({ ...trans, id: 'three' });
await updateCategoryRules([{ ...trans, id: 'three' }]);
expect(getRules().length).toBe(3);
trans = {
date: '2016-12-02',
account: 'acct',
payee: 'foo',
category: 'beer'
};
await db.insertTransaction({ ...trans, id: 'four' });
await db.insertTransaction({ ...trans, id: 'five' });
await db.insertTransaction({ ...trans, id: 'six' });
await updateCategoryRules([{ ...trans, id: 'three' }]);
expect(getRules().length).toBe(3);
let rules = getRules();
let getPayees = cat => {
let arr = rules
.filter(rule => rule.actions[0].value === cat)
.map(r => r.conditions.map(c => c.value));
return Array.prototype.concat.apply([], arr);
};
// The `foo` payee has been removed from all rules and added to
// the correct one
expect(getPayees('unknown1')).toEqual([]);
expect(getPayees('unknown2')).toEqual([]);
expect(getPayees('food')).toEqual([]);
expect(getPayees('beer')).toEqual(['foo', 'foo', null]);
});
test('avoids remembering categories for `null` payee', async () => {
await loadData();
expect(getRules().length).toBe(0);
let trans = {
date: '2016-12-01',
account: 'acct',
payee: null,
category: 'food'
};
await db.insertTransaction({ ...trans, id: 'one' });
await db.insertTransaction({ ...trans, id: 'two' });
await db.insertTransaction({ ...trans, id: 'three' });
await updateCategoryRules([{ ...trans, id: 'three' }]);
expect(getRules().length).toBe(0);
});
test('adding transaction with `null` payee never changes rules', async () => {
await loadData();
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'payee', value: 'foo' }],
actions: [{ op: 'set', field: 'category', value: 'unknown1' }]
});
await insertRule({
stage: null,
conditions: [{ op: 'oneOf', field: 'payee', value: ['foo', 'bar'] }],
actions: [{ op: 'set', field: 'category', value: 'unknown1' }]
});
expect(getRules().length).toBe(2);
let trans = {
date: '2016-12-01',
account: 'acct',
payee: null,
category: 'food'
};
await db.insertTransaction({ ...trans, id: 'one' });
await db.insertTransaction({ ...trans, id: 'two' });
await db.insertTransaction({ ...trans, id: 'three' });
await updateCategoryRules([{ ...trans, id: 'three' }]);
// This should not have changed the category! This is tested
// because this was a bug when rules were released
let rules = getRules();
expect(rules.length).toBe(2);
expect(rules[0].actions[0].value).toBe('unknown1');
expect(rules[1].actions[0].value).toBe('unknown1');
});
test('rules are saved with internal field names', async () => {
await insertRule({
stage: null,
conditions: [{ op: 'is', field: 'imported_payee', value: 'foo' }],
actions: [{ op: 'set', field: 'payee', value: 'unknown1' }]
});
// The rule that the system sees should use the new public names
let [rule] = getRules();
expect(rule.conditions[0].field).toBe('imported_payee');
expect(rule.actions[0].field).toBe('payee');
// Internally, it should still be stored with the internal names
// so that it's backwards compatible
let rawRule = await db.first('SELECT * FROM rules');
rawRule.conditions = JSON.parse(rawRule.conditions);
rawRule.actions = JSON.parse(rawRule.actions);
expect(rawRule.conditions[0].field).toBe('imported_description');
expect(rawRule.actions[0].field).toBe('description');
await loadRules();
// Make sure reloading everything from the db still uses the new
// public names
[rule] = getRules();
expect(rule.conditions[0].field).toBe('imported_payee');
expect(rule.actions[0].field).toBe('payee');
});
test('rules with public field names are loaded correctly', async () => {
await db.insertWithUUID('rules', {
stage: null,
conditions: JSON.stringify([
{ op: 'is', field: 'imported_payee', value: 'foo' }
]),
actions: JSON.stringify([{ op: 'set', field: 'payee', value: 'payee1' }])
});
await loadRules();
// This rule internally has been stored with the public names.
// Making this work now allows us to switch to it by default in
// the future
let rawRule = await db.first('SELECT * FROM rules');
rawRule.conditions = JSON.parse(rawRule.conditions);
rawRule.actions = JSON.parse(rawRule.actions);
expect(rawRule.conditions[0].field).toBe('imported_payee');
expect(rawRule.actions[0].field).toBe('payee');
let [rule] = getRules();
expect(rule.conditions[0].field).toBe('imported_payee');
expect(rule.actions[0].field).toBe('payee');
});
// TODO: write tests for split transactions
});