Skip to content

Commit

Permalink
feat: templating on payee_name (#3619)
Browse files Browse the repository at this point in the history
* feat: templating on payee_name

* chore: release note

* fix: create test budget

* fix: transaction-rules tests

* fix: transaction-rules tests
  • Loading branch information
UnderKoen authored Oct 15, 2024
1 parent b253246 commit 3d9e90f
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 19 deletions.
10 changes: 8 additions & 2 deletions packages/desktop-client/src/components/modals/EditRuleModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ function ScheduleDescription({ id }) {
const actionFields = [
'category',
'payee',
'payee_name',
'notes',
'cleared',
'account',
Expand All @@ -382,7 +383,12 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
const templated = options?.template !== undefined;

// Even if the feature flag is disabled, we still want to be able to turn off templating
const isTemplatingEnabled = useFeatureFlag('actionTemplating') || templated;
const actionTemplating = useFeatureFlag('actionTemplating');
const isTemplatingEnabled = actionTemplating || templated;

const fields = (
options?.splitIndex ? splitActionFields : actionFields
).filter(([s]) => actionTemplating || !s.includes('_name') || field === s);

return (
<Editor style={editorStyle} error={error}>
Expand All @@ -395,7 +401,7 @@ function ActionEditor({ action, editorStyle, onChange, onDelete, onAdd }) {
/>

<FieldSelect
fields={options?.splitIndex ? splitActionFields : actionFields}
fields={fields}
value={field}
onChange={value => onChange('field', value)}
/>
Expand Down
1 change: 1 addition & 0 deletions packages/desktop-client/src/components/rules/Value.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function Value<T>({
return value ? formatDate(parseISO(value), 'yyyy') : null;
case 'notes':
case 'imported_payee':
case 'payee_name':
return value;
case 'payee':
case 'category':
Expand Down
4 changes: 4 additions & 0 deletions packages/loot-core/src/server/accounts/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ function registerHandlebarsHelpers() {

const helpers = {
regex: (value: unknown, regex: unknown, replace: unknown) => {
if (value == null) {
return null;
}

if (typeof regex !== 'string' || typeof replace !== 'string') {
return '';
}
Expand Down
4 changes: 2 additions & 2 deletions packages/loot-core/src/server/accounts/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ export async function matchTransactions(
subtransactions,
} of normalized) {
// Run the rules
const trans = runRules(originalTrans);
const trans = await runRules(originalTrans);

let match = null;
let fuzzyDataset = null;
Expand Down Expand Up @@ -605,7 +605,7 @@ export async function addTransactions(

for (const { trans: originalTrans, subtransactions } of normalized) {
// Run the rules
const trans = runRules(originalTrans);
const trans = await runRules(originalTrans);

const finalTransaction = {
id: uuidv4(),
Expand Down
20 changes: 10 additions & 10 deletions packages/loot-core/src/server/accounts/transaction-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('Transaction rules', () => {
spy.mockRestore();

// Finally make sure the rule is actually in place and runs
const transaction = runRules({
const transaction = await runRules({
date: '2019-05-10',
notes: '',
category: null,
Expand All @@ -149,7 +149,7 @@ describe('Transaction rules', () => {
});
expect(getRules().length).toBe(1);

let transaction = runRules({
let transaction = await runRules({
imported_payee: 'Kroger',
notes: '',
category: null,
Expand All @@ -165,7 +165,7 @@ describe('Transaction rules', () => {
});
expect(getRules().length).toBe(1);

transaction = runRules({
transaction = await runRules({
imported_payee: 'Kroger',
notes: '',
category: null,
Expand All @@ -179,7 +179,7 @@ describe('Transaction rules', () => {
id,
conditions: [{ op: 'is', field: 'imported_payee', value: 'ABC' }],
});
transaction = runRules({
transaction = await runRules({
imported_payee: 'ABC',
notes: '',
category: null,
Expand All @@ -201,7 +201,7 @@ describe('Transaction rules', () => {
});
expect(getRules().length).toBe(1);

let transaction = runRules({
let transaction = await runRules({
payee: 'Kroger',
notes: '',
category: null,
Expand All @@ -211,7 +211,7 @@ describe('Transaction rules', () => {

await deleteRule(id);
expect(getRules().length).toBe(0);
transaction = runRules({
transaction = await runRules({
payee: 'Kroger',
notes: '',
category: null,
Expand Down Expand Up @@ -242,14 +242,14 @@ describe('Transaction rules', () => {
await loadRules();
expect(getRules().length).toBe(2);

let transaction = runRules({
let transaction = await runRules({
imported_payee: 'blah Lowes blah',
payee: null,
category: null,
});
expect(transaction.payee).toBe('lowes');

transaction = runRules({
transaction = await runRules({
imported_payee: 'kroger',
category: null,
});
Expand Down Expand Up @@ -315,7 +315,7 @@ describe('Transaction rules', () => {
expect(rule2.conditions[1].value).toBe('beer_id');
});

test('runRules runs all the rules in each phase', async () => {
test('await runRules runs all the rules in each phase', async () => {
await loadRules();
await insertRule({
stage: 'post',
Expand Down Expand Up @@ -354,7 +354,7 @@ describe('Transaction rules', () => {
});

expect(
runRules({
await runRules({
imported_payee: '123 kroger',
date: '2020-08-11',
amount: 50,
Expand Down
59 changes: 54 additions & 5 deletions packages/loot-core/src/server/accounts/transaction-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../types/models';
import { schemaConfig } from '../aql';
import * as db from '../db';
import { getPayee, getPayeeByName, insertPayee } from '../db';
import { getMappings } from '../db/mappings';
import { RuleError } from '../errors';
import { requiredFields, toDateRepr } from '../models';
Expand Down Expand Up @@ -273,8 +274,8 @@ function onApplySync(oldValues, newValues) {
}

// Runner
export function runRules(trans) {
let finalTrans = { ...trans };
export async function runRules(trans) {
let finalTrans = await prepareTransactionForRules({ ...trans });

const rules = rankRules(
fastSetMerge(
Expand All @@ -287,7 +288,7 @@ export function runRules(trans) {
finalTrans = rules[i].apply(finalTrans);
}

return finalTrans;
return await finalizeTransactionForRules(finalTrans);
}

// This does the inverse: finds all the transactions matching a rule
Expand Down Expand Up @@ -539,11 +540,20 @@ export async function applyActions(
return null;
}

const updated = transactions.flatMap(trans => {
const transactionsForRules = await Promise.all(
transactions.map(prepareTransactionForRules),
);

const updated = transactionsForRules.flatMap(trans => {
return ungroupTransaction(execActions(parsedActions, trans));
});

return batchUpdateTransactions({ updated });
const finalized: TransactionEntity[] = [];
for (const trans of updated) {
finalized.push(await finalizeTransactionForRules(trans));
}

return batchUpdateTransactions({ updated: finalized });
}

export function getRulesForPayee(payeeId) {
Expand Down Expand Up @@ -759,3 +769,42 @@ export async function updateCategoryRules(transactions) {
}
});
}

export type TransactionForRules = TransactionEntity & {
payee_name?: string;
};

export async function prepareTransactionForRules(
trans: TransactionEntity,
): Promise<TransactionForRules> {
const r: TransactionForRules = { ...trans };
if (trans.payee) {
const payee = await getPayee(trans.payee);
if (payee) {
r.payee_name = payee.name;
}
}

return r;
}

export async function finalizeTransactionForRules(
trans: TransactionEntity | TransactionForRules,
): Promise<TransactionEntity> {
if ('payee_name' in trans) {
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;
}

delete trans.payee_name;
}

return trans;
}
3 changes: 3 additions & 0 deletions packages/loot-core/src/shared/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const FIELD_INFO = {
disallowedOps: new Set(['hasTags']),
},
payee: { type: 'id' },
payee_name: { type: 'string' },
date: { type: 'date' },
notes: { type: 'string' },
amount: { type: 'number' },
Expand Down Expand Up @@ -112,6 +113,8 @@ export function mapField(field, opts?) {
switch (field) {
case 'imported_payee':
return 'imported payee';
case 'payee_name':
return 'payee (name)';
case 'amount':
if (opts.inflow) {
return 'amount (inflow)';
Expand Down
1 change: 1 addition & 0 deletions packages/loot-core/src/types/models/rule.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type FieldValueTypes = {
date: string;
notes: string;
payee: string;
payee_name: string;
imported_payee: string;
saved: string;
cleared: boolean;
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/3619.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [UnderKoen]
---

Add action rule templating for `payee_name`

0 comments on commit 3d9e90f

Please sign in to comment.