From 193a86cf306e83bcc3a31030c0f82cb776ca1c0c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 12 Aug 2024 17:53:57 +0200 Subject: [PATCH 1/3] feat: add amount comparators to amount bank rule field --- .../Banking/BankingRulesController.ts | 11 ++++- .../Banking/RegonizeTranasctions/_utils.ts | 45 ++++++++++++------- .../src/services/Banking/Rules/types.ts | 19 ++++++-- .../RuleFormDialog/RuleFormContentForm.tsx | 19 +++++++- .../Banking/Rules/RuleFormDialog/_utils.ts | 33 +++++++++++++- 5 files changed, 103 insertions(+), 24 deletions(-) diff --git a/packages/server/src/api/controllers/Banking/BankingRulesController.ts b/packages/server/src/api/controllers/Banking/BankingRulesController.ts index e7608f56d..008b753d9 100644 --- a/packages/server/src/api/controllers/Banking/BankingRulesController.ts +++ b/packages/server/src/api/controllers/Banking/BankingRulesController.ts @@ -33,7 +33,16 @@ export class BankingRulesController extends BaseController { body('conditions.*.field').exists().isIn(['description', 'amount']), body('conditions.*.comparator') .exists() - .isIn(['equals', 'contains', 'not_contain']) + .isIn([ + 'equals', + 'equal', + 'contains', + 'not_contain', + 'bigger', + 'bigger_or_equal', + 'smaller', + 'smaller_or_equal', + ]) .default('contain') .trim(), body('conditions.*.value').exists().trim(), diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts b/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts index 1eaa546ec..a69aebe5f 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts @@ -33,17 +33,29 @@ const matchNumberCondition = ( transaction: UncategorizedCashflowTransaction, condition: IBankRuleCondition ) => { + const conditionValue = parseFloat(condition.value); + const transactionAmount = + condition.field === 'amount' + ? Math.abs(transaction[condition.field]) + : (transaction[condition.field] as unknown as number); + switch (condition.comparator) { case BankRuleConditionComparator.Equals: - return transaction[condition.field] === condition.value; - case BankRuleConditionComparator.Contains: - return transaction[condition.field] - ?.toString() - .includes(condition.value.toString()); - case BankRuleConditionComparator.NotContain: - return !transaction[condition.field] - ?.toString() - .includes(condition.value.toString()); + case BankRuleConditionComparator.Equal: + return transactionAmount === conditionValue; + + case BankRuleConditionComparator.BiggerOrEqual: + return transactionAmount >= conditionValue; + + case BankRuleConditionComparator.Bigger: + return transactionAmount > conditionValue; + + case BankRuleConditionComparator.Smaller: + return transactionAmount < conditionValue; + + case BankRuleConditionComparator.SmallerOrEqual: + return transactionAmount <= conditionValue; + default: return false; } @@ -53,18 +65,19 @@ const matchTextCondition = ( transaction: UncategorizedCashflowTransaction, condition: IBankRuleCondition ): boolean => { + const transactionValue = transaction[condition.field] as string; + switch (condition.comparator) { case BankRuleConditionComparator.Equals: - return transaction[condition.field] === condition.value; + case BankRuleConditionComparator.Equal: + return transactionValue === condition.value; case BankRuleConditionComparator.Contains: - const fieldValue = lowerCase(transaction[condition.field]); + const fieldValue = lowerCase(transactionValue); const conditionValue = lowerCase(condition.value); return fieldValue.includes(conditionValue); case BankRuleConditionComparator.NotContain: - return !transaction[condition.field]?.includes( - condition.value.toString() - ); + return !transactionValue?.includes(condition.value.toString()); default: return false; } @@ -101,8 +114,8 @@ const determineFieldType = (field: string): string => { case 'amount': return 'number'; case 'description': - return 'text'; + case 'payee': default: return 'unknown'; } -}; \ No newline at end of file +}; diff --git a/packages/server/src/services/Banking/Rules/types.ts b/packages/server/src/services/Banking/Rules/types.ts index d54cb9388..7f8bb6cbc 100644 --- a/packages/server/src/services/Banking/Rules/types.ts +++ b/packages/server/src/services/Banking/Rules/types.ts @@ -1,15 +1,20 @@ import { Knex } from 'knex'; export enum BankRuleConditionField { - Amount = 'Amount', - Description = 'Description', - Payee = 'Payee', + Amount = 'amount', + Description = 'description', + Payee = 'payee', } export enum BankRuleConditionComparator { Contains = 'contains', Equals = 'equals', + Equal = 'equal', NotContain = 'not_contain', + Bigger = 'bigger', + BiggerOrEqual = 'bigger_or_equal', + Smaller = 'smaller', + SmallerOrEqual = 'smaller_or_equal', } export interface IBankRuleCondition { @@ -56,7 +61,13 @@ export enum BankRuleAssignCategory { export interface IBankRuleConditionDTO { id?: number; field: string; - comparator: string; + comparator: + | 'contains' + | 'equals' + | 'not_contains' + | 'smaller' + | 'bigger_or_equal' + | 'smaller_or_equal'; value: number; } diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx index 96b5f4364..b27b86f50 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx @@ -17,11 +17,12 @@ import { } from '@/components'; import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules'; import { - FieldCondition, Fields, RuleFormValues, TransactionTypeOptions, getAccountRootFromMoneyCategory, + getDefaultFieldConditionByFieldKey, + getFieldConditionsByFieldKey, initialValues, } from './_utils'; import { useRuleFormDialogBoot } from './RuleFormBoot'; @@ -33,6 +34,7 @@ import { import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants'; +import { get } from 'lodash'; // Retrieves the add money in button options. const MoneyInOptions = getAddMoneyInOptions(); @@ -175,6 +177,13 @@ function RuleFormConditions() { setFieldValue('conditions', _conditions); }; + const handleConditionFieldChange = (item) => { + const defaultComparator = getDefaultFieldConditionByFieldKey(item.value); + + setFieldValue(`conditions[${index}].field`, item.value); + setFieldValue(`conditions[${index}].comparator`, defaultComparator); + }; + return ( @@ -190,6 +199,7 @@ function RuleFormConditions() { name={`conditions[${index}].field`} items={Fields} popoverProps={{ minimal: true, inline: false }} + onItemChange={handleConditionFieldChange} fastField /> @@ -202,8 +212,13 @@ function RuleFormConditions() { > diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts index db77154e9..343d9d5a3 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts @@ -42,11 +42,24 @@ export const Fields = [ { value: 'amount', text: 'Amount' }, { value: 'payee', text: 'Payee' }, ]; -export const FieldCondition = [ + +export const TextFieldConditions = [ { value: 'contains', text: 'Contains' }, { value: 'equals', text: 'Equals' }, { value: 'not_contains', text: 'Not Contains' }, ]; +export const NumberFieldConditions = [ + { value: 'bigger', text: 'Bigger' }, + { value: 'bigger_or_equal', text: 'Bigger or Equal' }, + { value: 'smaller', text: 'Smaller' }, + { value: 'smaller_or_equal', text: 'Smaller or Equal' }, +]; + +export const FieldCondition = [ + ...TextFieldConditions, + ...NumberFieldConditions, +]; + export const AssignTransactionTypeOptions = [ { value: 'expense', text: 'Expense' }, ]; @@ -56,3 +69,21 @@ export const getAccountRootFromMoneyCategory = (category: string): string[] => { return get(MoneyCategoryPerCreditAccountRootType, _category) || []; }; + +export const getFieldConditionsByFieldKey = (fieldKey?: string) => { + switch (fieldKey) { + case 'amount': + return NumberFieldConditions; + default: + return TextFieldConditions; + } +}; + +export const getDefaultFieldConditionByFieldKey = (fieldKey?: string) => { + switch (fieldKey) { + case 'amount': + return 'bigger_or_equal'; + default: + return 'equals'; + } +}; From cf4bb3007ed06c1d3e2aa772d28d897b50805413 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 12 Aug 2024 20:07:01 +0200 Subject: [PATCH 2/3] feat: run re-recognizing bank transactions on edit bank rule --- .../services/Banking/RegonizeTranasctions/_utils.ts | 2 +- .../events/TriggerRecognizedTransactions.ts | 13 +++++++++++++ .../src/services/Banking/Rules/EditBankRule.ts | 5 +++-- packages/server/src/services/Banking/Rules/types.ts | 2 ++ .../Rules/RuleFormDialog/RuleFormContentForm.tsx | 8 ++++---- .../Banking/Rules/RuleFormDialog/_utils.ts | 2 +- 6 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts b/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts index a69aebe5f..4ca0d8b9f 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/_utils.ts @@ -116,6 +116,6 @@ const determineFieldType = (field: string): string => { case 'description': case 'payee': default: - return 'unknown'; + return 'text'; } }; diff --git a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts index 0ee073a46..466f0b3b1 100644 --- a/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts +++ b/packages/server/src/services/Banking/RegonizeTranasctions/events/TriggerRecognizedTransactions.ts @@ -1,4 +1,5 @@ import { Inject, Service } from 'typedi'; +import { isEqual, omit } from 'lodash'; import events from '@/subscribers/events'; import { IBankRuleEventCreatedPayload, @@ -55,10 +56,22 @@ export class TriggerRecognizedTransactions { private async recognizedTransactionsOnRuleEdited({ tenantId, editRuleDTO, + oldBankRule, + bankRule, ruleId, }: IBankRuleEventEditedPayload) { const payload = { tenantId, ruleId }; + // Cannot continue if the new and old bank rule values are the same, + // after excluding `createdAt` and `updatedAt` dates. + if ( + isEqual( + omit(bankRule, ['createdAt', 'updatedAt']), + omit(oldBankRule, ['createdAt', 'updatedAt']) + ) + ) { + return; + } await this.agenda.now( 'rerecognize-uncategorized-transactions-job', payload diff --git a/packages/server/src/services/Banking/Rules/EditBankRule.ts b/packages/server/src/services/Banking/Rules/EditBankRule.ts index 2d6d868f4..67536ab82 100644 --- a/packages/server/src/services/Banking/Rules/EditBankRule.ts +++ b/packages/server/src/services/Banking/Rules/EditBankRule.ts @@ -47,6 +47,7 @@ export class EditBankRuleService { const oldBankRule = await BankRule.query() .findById(ruleId) + .withGraphFetched('conditions') .throwIfNotFound(); const tranformDTO = this.transformDTO(editRuleDTO); @@ -64,15 +65,15 @@ export class EditBankRuleService { } as IBankRuleEventEditingPayload); // Updates the given bank rule. - await BankRule.query(trx).upsertGraphAndFetch({ + const bankRule = await BankRule.query(trx).upsertGraphAndFetch({ ...tranformDTO, id: ruleId, }); - // Triggers `onBankRuleEdited` event. await this.eventPublisher.emitAsync(events.bankRules.onEdited, { tenantId, oldBankRule, + bankRule, ruleId, editRuleDTO, trx, diff --git a/packages/server/src/services/Banking/Rules/types.ts b/packages/server/src/services/Banking/Rules/types.ts index 7f8bb6cbc..f13670db8 100644 --- a/packages/server/src/services/Banking/Rules/types.ts +++ b/packages/server/src/services/Banking/Rules/types.ts @@ -110,6 +110,8 @@ export interface IBankRuleEventEditingPayload { export interface IBankRuleEventEditedPayload { tenantId: number; ruleId: number; + oldBankRule: IBankRule; + bankRule: IBankRule; editRuleDTO: IEditBankRuleDTO; trx?: Knex.Transaction; } diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx index b27b86f50..ee9820772 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx @@ -177,12 +177,12 @@ function RuleFormConditions() { setFieldValue('conditions', _conditions); }; - const handleConditionFieldChange = (item) => { + const handleConditionFieldChange = R.curry((index, item) => { const defaultComparator = getDefaultFieldConditionByFieldKey(item.value); - + setFieldValue(`conditions[${index}].field`, item.value); setFieldValue(`conditions[${index}].comparator`, defaultComparator); - }; + }); return ( @@ -199,7 +199,7 @@ function RuleFormConditions() { name={`conditions[${index}].field`} items={Fields} popoverProps={{ minimal: true, inline: false }} - onItemChange={handleConditionFieldChange} + onItemChange={handleConditionFieldChange(index)} fastField /> diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts index 343d9d5a3..b3ca5b924 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts @@ -84,6 +84,6 @@ export const getDefaultFieldConditionByFieldKey = (fieldKey?: string) => { case 'amount': return 'bigger_or_equal'; default: - return 'equals'; + return 'contains'; } }; From c1b29c3f23d86ce8daf3c04ceabd9677782658d0 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 12 Aug 2024 20:16:18 +0200 Subject: [PATCH 3/3] fix: add equal condition to number fields on bank rule --- packages/server/src/services/Banking/Rules/types.ts | 4 +++- .../Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx | 2 +- .../src/containers/Banking/Rules/RuleFormDialog/_utils.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/server/src/services/Banking/Rules/types.ts b/packages/server/src/services/Banking/Rules/types.ts index f13670db8..49e71abf5 100644 --- a/packages/server/src/services/Banking/Rules/types.ts +++ b/packages/server/src/services/Banking/Rules/types.ts @@ -65,8 +65,10 @@ export interface IBankRuleConditionDTO { | 'contains' | 'equals' | 'not_contains' - | 'smaller' + | 'equal' + | 'bigger' | 'bigger_or_equal' + | 'smaller' | 'smaller_or_equal'; value: number; } diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx index ee9820772..f3b172b7f 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/RuleFormContentForm.tsx @@ -1,6 +1,7 @@ // @ts-nocheck import { useCallback, useMemo } from 'react'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { get } from 'lodash'; import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core'; import * as R from 'ramda'; import { CreateRuleFormSchema } from './RuleFormContentForm.schema'; @@ -34,7 +35,6 @@ import { import withDialogActions from '@/containers/Dialog/withDialogActions'; import { DialogsName } from '@/constants/dialogs'; import { getAddMoneyInOptions, getAddMoneyOutOptions } from '@/constants'; -import { get } from 'lodash'; // Retrieves the add money in button options. const MoneyInOptions = getAddMoneyInOptions(); diff --git a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts index b3ca5b924..e68418d39 100644 --- a/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts +++ b/packages/webapp/src/containers/Banking/Rules/RuleFormDialog/_utils.ts @@ -49,6 +49,7 @@ export const TextFieldConditions = [ { value: 'not_contains', text: 'Not Contains' }, ]; export const NumberFieldConditions = [ + { value: 'equal', text: 'Equal' }, { value: 'bigger', text: 'Bigger' }, { value: 'bigger_or_equal', text: 'Bigger or Equal' }, { value: 'smaller', text: 'Smaller' },