Merge pull request #595 from bigcapitalhq/add-comparators-to-amount-bank-rule

feat: Add amount comparators to amount bank rule field
This commit is contained in:
Ahmed Bouhuolia
2024-08-12 20:17:20 +02:00
committed by GitHub
7 changed files with 125 additions and 27 deletions

View File

@@ -33,7 +33,16 @@ export class BankingRulesController extends BaseController {
body('conditions.*.field').exists().isIn(['description', 'amount']), body('conditions.*.field').exists().isIn(['description', 'amount']),
body('conditions.*.comparator') body('conditions.*.comparator')
.exists() .exists()
.isIn(['equals', 'contains', 'not_contain']) .isIn([
'equals',
'equal',
'contains',
'not_contain',
'bigger',
'bigger_or_equal',
'smaller',
'smaller_or_equal',
])
.default('contain') .default('contain')
.trim(), .trim(),
body('conditions.*.value').exists().trim(), body('conditions.*.value').exists().trim(),

View File

@@ -33,17 +33,29 @@ const matchNumberCondition = (
transaction: UncategorizedCashflowTransaction, transaction: UncategorizedCashflowTransaction,
condition: IBankRuleCondition 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) { switch (condition.comparator) {
case BankRuleConditionComparator.Equals: case BankRuleConditionComparator.Equals:
return transaction[condition.field] === condition.value; case BankRuleConditionComparator.Equal:
case BankRuleConditionComparator.Contains: return transactionAmount === conditionValue;
return transaction[condition.field]
?.toString() case BankRuleConditionComparator.BiggerOrEqual:
.includes(condition.value.toString()); return transactionAmount >= conditionValue;
case BankRuleConditionComparator.NotContain:
return !transaction[condition.field] case BankRuleConditionComparator.Bigger:
?.toString() return transactionAmount > conditionValue;
.includes(condition.value.toString());
case BankRuleConditionComparator.Smaller:
return transactionAmount < conditionValue;
case BankRuleConditionComparator.SmallerOrEqual:
return transactionAmount <= conditionValue;
default: default:
return false; return false;
} }
@@ -53,18 +65,19 @@ const matchTextCondition = (
transaction: UncategorizedCashflowTransaction, transaction: UncategorizedCashflowTransaction,
condition: IBankRuleCondition condition: IBankRuleCondition
): boolean => { ): boolean => {
const transactionValue = transaction[condition.field] as string;
switch (condition.comparator) { switch (condition.comparator) {
case BankRuleConditionComparator.Equals: case BankRuleConditionComparator.Equals:
return transaction[condition.field] === condition.value; case BankRuleConditionComparator.Equal:
return transactionValue === condition.value;
case BankRuleConditionComparator.Contains: case BankRuleConditionComparator.Contains:
const fieldValue = lowerCase(transaction[condition.field]); const fieldValue = lowerCase(transactionValue);
const conditionValue = lowerCase(condition.value); const conditionValue = lowerCase(condition.value);
return fieldValue.includes(conditionValue); return fieldValue.includes(conditionValue);
case BankRuleConditionComparator.NotContain: case BankRuleConditionComparator.NotContain:
return !transaction[condition.field]?.includes( return !transactionValue?.includes(condition.value.toString());
condition.value.toString()
);
default: default:
return false; return false;
} }
@@ -101,8 +114,8 @@ const determineFieldType = (field: string): string => {
case 'amount': case 'amount':
return 'number'; return 'number';
case 'description': case 'description':
return 'text'; case 'payee':
default: default:
return 'unknown'; return 'text';
} }
}; };

View File

@@ -1,4 +1,5 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { isEqual, omit } from 'lodash';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { import {
IBankRuleEventCreatedPayload, IBankRuleEventCreatedPayload,
@@ -55,10 +56,22 @@ export class TriggerRecognizedTransactions {
private async recognizedTransactionsOnRuleEdited({ private async recognizedTransactionsOnRuleEdited({
tenantId, tenantId,
editRuleDTO, editRuleDTO,
oldBankRule,
bankRule,
ruleId, ruleId,
}: IBankRuleEventEditedPayload) { }: IBankRuleEventEditedPayload) {
const payload = { tenantId, ruleId }; 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( await this.agenda.now(
'rerecognize-uncategorized-transactions-job', 'rerecognize-uncategorized-transactions-job',
payload payload

View File

@@ -47,6 +47,7 @@ export class EditBankRuleService {
const oldBankRule = await BankRule.query() const oldBankRule = await BankRule.query()
.findById(ruleId) .findById(ruleId)
.withGraphFetched('conditions')
.throwIfNotFound(); .throwIfNotFound();
const tranformDTO = this.transformDTO(editRuleDTO); const tranformDTO = this.transformDTO(editRuleDTO);
@@ -64,15 +65,15 @@ export class EditBankRuleService {
} as IBankRuleEventEditingPayload); } as IBankRuleEventEditingPayload);
// Updates the given bank rule. // Updates the given bank rule.
await BankRule.query(trx).upsertGraphAndFetch({ const bankRule = await BankRule.query(trx).upsertGraphAndFetch({
...tranformDTO, ...tranformDTO,
id: ruleId, id: ruleId,
}); });
// Triggers `onBankRuleEdited` event. // Triggers `onBankRuleEdited` event.
await this.eventPublisher.emitAsync(events.bankRules.onEdited, { await this.eventPublisher.emitAsync(events.bankRules.onEdited, {
tenantId, tenantId,
oldBankRule, oldBankRule,
bankRule,
ruleId, ruleId,
editRuleDTO, editRuleDTO,
trx, trx,

View File

@@ -1,15 +1,20 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
export enum BankRuleConditionField { export enum BankRuleConditionField {
Amount = 'Amount', Amount = 'amount',
Description = 'Description', Description = 'description',
Payee = 'Payee', Payee = 'payee',
} }
export enum BankRuleConditionComparator { export enum BankRuleConditionComparator {
Contains = 'contains', Contains = 'contains',
Equals = 'equals', Equals = 'equals',
Equal = 'equal',
NotContain = 'not_contain', NotContain = 'not_contain',
Bigger = 'bigger',
BiggerOrEqual = 'bigger_or_equal',
Smaller = 'smaller',
SmallerOrEqual = 'smaller_or_equal',
} }
export interface IBankRuleCondition { export interface IBankRuleCondition {
@@ -56,7 +61,15 @@ export enum BankRuleAssignCategory {
export interface IBankRuleConditionDTO { export interface IBankRuleConditionDTO {
id?: number; id?: number;
field: string; field: string;
comparator: string; comparator:
| 'contains'
| 'equals'
| 'not_contains'
| 'equal'
| 'bigger'
| 'bigger_or_equal'
| 'smaller'
| 'smaller_or_equal';
value: number; value: number;
} }
@@ -99,6 +112,8 @@ export interface IBankRuleEventEditingPayload {
export interface IBankRuleEventEditedPayload { export interface IBankRuleEventEditedPayload {
tenantId: number; tenantId: number;
ruleId: number; ruleId: number;
oldBankRule: IBankRule;
bankRule: IBankRule;
editRuleDTO: IEditBankRuleDTO; editRuleDTO: IEditBankRuleDTO;
trx?: Knex.Transaction; trx?: Knex.Transaction;
} }

View File

@@ -1,6 +1,7 @@
// @ts-nocheck // @ts-nocheck
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { get } from 'lodash';
import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core'; import { Button, Classes, Intent, Radio, Tag } from '@blueprintjs/core';
import * as R from 'ramda'; import * as R from 'ramda';
import { CreateRuleFormSchema } from './RuleFormContentForm.schema'; import { CreateRuleFormSchema } from './RuleFormContentForm.schema';
@@ -17,11 +18,12 @@ import {
} from '@/components'; } from '@/components';
import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules'; import { useCreateBankRule, useEditBankRule } from '@/hooks/query/bank-rules';
import { import {
FieldCondition,
Fields, Fields,
RuleFormValues, RuleFormValues,
TransactionTypeOptions, TransactionTypeOptions,
getAccountRootFromMoneyCategory, getAccountRootFromMoneyCategory,
getDefaultFieldConditionByFieldKey,
getFieldConditionsByFieldKey,
initialValues, initialValues,
} from './_utils'; } from './_utils';
import { useRuleFormDialogBoot } from './RuleFormBoot'; import { useRuleFormDialogBoot } from './RuleFormBoot';
@@ -175,6 +177,13 @@ function RuleFormConditions() {
setFieldValue('conditions', _conditions); setFieldValue('conditions', _conditions);
}; };
const handleConditionFieldChange = R.curry((index, item) => {
const defaultComparator = getDefaultFieldConditionByFieldKey(item.value);
setFieldValue(`conditions[${index}].field`, item.value);
setFieldValue(`conditions[${index}].comparator`, defaultComparator);
});
return ( return (
<Box style={{ marginBottom: 15 }}> <Box style={{ marginBottom: 15 }}>
<Stack spacing={15}> <Stack spacing={15}>
@@ -190,6 +199,7 @@ function RuleFormConditions() {
name={`conditions[${index}].field`} name={`conditions[${index}].field`}
items={Fields} items={Fields}
popoverProps={{ minimal: true, inline: false }} popoverProps={{ minimal: true, inline: false }}
onItemChange={handleConditionFieldChange(index)}
fastField fastField
/> />
</FFormGroup> </FFormGroup>
@@ -202,8 +212,13 @@ function RuleFormConditions() {
> >
<FSelect <FSelect
name={`conditions[${index}].comparator`} name={`conditions[${index}].comparator`}
items={FieldCondition} items={getFieldConditionsByFieldKey(
get(values, `conditions[${index}].field`),
)}
popoverProps={{ minimal: true, inline: false }} popoverProps={{ minimal: true, inline: false }}
shouldUpdateDeps={{
fieldKey: get(values, `conditions[${index}].field`),
}}
fastField fastField
/> />
</FFormGroup> </FFormGroup>

View File

@@ -42,11 +42,25 @@ export const Fields = [
{ value: 'amount', text: 'Amount' }, { value: 'amount', text: 'Amount' },
{ value: 'payee', text: 'Payee' }, { value: 'payee', text: 'Payee' },
]; ];
export const FieldCondition = [
export const TextFieldConditions = [
{ value: 'contains', text: 'Contains' }, { value: 'contains', text: 'Contains' },
{ value: 'equals', text: 'Equals' }, { value: 'equals', text: 'Equals' },
{ value: 'not_contains', text: 'Not Contains' }, { 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' },
{ value: 'smaller_or_equal', text: 'Smaller or Equal' },
];
export const FieldCondition = [
...TextFieldConditions,
...NumberFieldConditions,
];
export const AssignTransactionTypeOptions = [ export const AssignTransactionTypeOptions = [
{ value: 'expense', text: 'Expense' }, { value: 'expense', text: 'Expense' },
]; ];
@@ -56,3 +70,21 @@ export const getAccountRootFromMoneyCategory = (category: string): string[] => {
return get(MoneyCategoryPerCreditAccountRootType, _category) || []; 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 'contains';
}
};