Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: templating on payee_name #3619

Merged
merged 5 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -347,6 +347,7 @@ function ScheduleDescription({ id }) {
const actionFields = [
'category',
'payee',
'payee_name',
'notes',
'cleared',
'account',
Expand All @@ -372,7 +373,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 @@ -385,7 +391,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);
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved

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);
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved

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
57 changes: 52 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 });
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved
}

export function getRulesForPayee(payeeId) {
Expand Down Expand Up @@ -759,3 +769,40 @@ 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);
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;
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved
} else {
trans.payee = null;
}

delete trans.payee_name;
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved
}

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;
UnderKoen marked this conversation as resolved.
Show resolved Hide resolved
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`
Loading