From 5b4ddadcf6c6826e0ce883da12fac1f5c8588de1 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 1 Mar 2024 17:12:56 +0200 Subject: [PATCH] feat(server): categorize the synced bank transactions --- .../Cashflow/NewCashflowTransaction.ts | 24 ++++++++++---- ...cateogrized_cashflow_transactions_table.js | 1 + .../UncategorizedCashflowTransaction.ts | 29 ++++++++++++++--- .../Cashflow/CategorizeCashflowTransaction.ts | 7 ++++ .../Cashflow/CommandCasflowValidator.ts | 32 +++++++++++++++++-- .../server/src/services/Cashflow/constants.ts | 3 +- 6 files changed, 82 insertions(+), 14 deletions(-) diff --git a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts index 9c48934ea..a3bd70b4f 100644 --- a/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/NewCashflowTransaction.ts @@ -1,5 +1,5 @@ import { Service, Inject } from 'typedi'; -import { check } from 'express-validator'; +import { check, oneOf } from 'express-validator'; import { Router, Request, Response, NextFunction } from 'express'; import BaseController from '../BaseController'; import { ServiceError } from '@/exceptions'; @@ -75,14 +75,16 @@ export default class NewCashflowTransactionController extends BaseController { public get categorizeCashflowTransactionValidationSchema() { return [ check('date').exists().isISO8601().toDate(), - - check('to_account_id').exists().isInt().toInt(), - check('from_account_id').exists().isInt().toInt(), - + oneOf([ + check('to_account_id').exists().isInt().toInt(), + check('from_account_id').exists().isInt().toInt(), + ]), + check('transaction_number').optional(), check('transaction_type').exists(), check('reference_no').optional(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('description').optional(), + check('branch_id').optional({ nullable: true }).isNumeric().toInt(), ]; } @@ -125,7 +127,7 @@ export default class NewCashflowTransactionController extends BaseController { const ownerContributionDTO = this.matchedBodyData(req); try { - const { cashflowTransaction } = + const cashflowTransaction = await this.newCashflowTranscationService.newCashflowTransaction( tenantId, ownerContributionDTO, @@ -301,6 +303,16 @@ export default class NewCashflowTransactionController extends BaseController { ], }); } + if (error.errorType === 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID') { + return res.boom.badRequest(null, { + errors: [ + { + type: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID', + code: 4100, + } + ] + }); + } } next(error); } diff --git a/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js b/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js index 2d72041f3..aab649b63 100644 --- a/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js +++ b/packages/server/src/database/migrations/20240228183404_create_uncateogrized_cashflow_transactions_table.js @@ -5,6 +5,7 @@ exports.up = function (knex) { table.increments('id'); table.date('date').index(); table.decimal('amount'); + table.string('currency_code'); table.string('reference_no').index(); table .integer('account_id') diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index ed00be47e..bb6798504 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -22,23 +22,42 @@ export default class UncategorizedCashflowTransaction extends TenantModel { * Retrieves the withdrawal amount. * @returns {number} */ - public withdrawal() { - return this.amount > 0 ? Math.abs(this.amount) : 0; + public get withdrawal() { + return this.amount < 0 ? Math.abs(this.amount) : 0; } /** * Retrieves the deposit amount. * @returns {number} */ - public deposit() { - return this.amount < 0 ? Math.abs(this.amount) : 0; + public get deposit(): number { + return this.amount > 0 ? Math.abs(this.amount) : 0; + } + + /** + * Detarmines whether the transaction is deposit transaction. + */ + public get isDepositTransaction(): boolean { + return 0 < this.deposit; + } + + /** + * Detarmines whether the transaction is withdrawal transaction. + */ + public get isWithdrawalTransaction(): boolean { + return 0 < this.withdrawal; } /** * Virtual attributes. */ static get virtualAttributes() { - return ['withdrawal', 'deposit']; + return [ + 'withdrawal', + 'deposit', + 'isDepositTransaction', + 'isWithdrawalTransaction', + ]; } /** diff --git a/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts index e965afede..da5b81419 100644 --- a/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts +++ b/packages/server/src/services/Cashflow/CategorizeCashflowTransaction.ts @@ -12,6 +12,7 @@ import { Knex } from 'knex'; import { transformCategorizeTransToCashflow } from './utils'; import { CommandCashflowValidator } from './CommandCasflowValidator'; import NewCashflowTransactionService from './NewCashflowTransactionService'; +import { TransferAuthorizationGuaranteeDecision } from 'plaid'; @Service() export class CategorizeCashflowTransaction { @@ -50,6 +51,12 @@ export class CategorizeCashflowTransaction { // Validates the transaction shouldn't be categorized before. this.commandValidators.validateTransactionShouldNotCategorized(transaction); + // Validate the uncateogirzed transaction if it's deposit the transaction direction + // should `IN` and the same thing if it's withdrawal the direction should be OUT. + this.commandValidators.validateUncategorizeTransactionType( + transaction, + categorizeDTO.transactionType + ); // Edits the cashflow transaction under UOW env. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Triggers `onTransactionCategorizing` event. diff --git a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts index aabdc6301..7a6c5c973 100644 --- a/packages/server/src/services/Cashflow/CommandCasflowValidator.ts +++ b/packages/server/src/services/Cashflow/CommandCasflowValidator.ts @@ -1,9 +1,13 @@ import { Service } from 'typedi'; import { includes, camelCase, upperFirst } from 'lodash'; -import { IAccount } from '@/interfaces'; +import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces'; import { getCashflowTransactionType } from './utils'; import { ServiceError } from '@/exceptions'; -import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants'; +import { + CASHFLOW_DIRECTION, + CASHFLOW_TRANSACTION_TYPE, + ERRORS, +} from './constants'; import CashflowTransaction from '@/models/CashflowTransaction'; @Service() @@ -71,4 +75,28 @@ export class CommandCashflowValidator { throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); } } + + /** + * + * @param {uncategorizeTransaction} + * @param {string} transactionType + * @throws {ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID)} + */ + public validateUncategorizeTransactionType( + uncategorizeTransaction: IUncategorizedCashflowTransaction, + transactionType: string + ) { + const type = getCashflowTransactionType( + upperFirst(camelCase(transactionType)) as CASHFLOW_TRANSACTION_TYPE + ); + if ( + (type.direction === CASHFLOW_DIRECTION.IN && + uncategorizeTransaction.isDepositTransaction) || + (type.direction === CASHFLOW_DIRECTION.OUT && + uncategorizeTransaction.isWithdrawalTransaction) + ) { + return; + } + throw new ServiceError(ERRORS.UNCATEGORIZED_TRANSACTION_TYPE_INVALID); + } } diff --git a/packages/server/src/services/Cashflow/constants.ts b/packages/server/src/services/Cashflow/constants.ts index 4ec2d7004..293275855 100644 --- a/packages/server/src/services/Cashflow/constants.ts +++ b/packages/server/src/services/Cashflow/constants.ts @@ -10,7 +10,8 @@ export const ERRORS = { ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE', ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions', TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', - TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED' + TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED', + UNCATEGORIZED_TRANSACTION_TYPE_INVALID: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID' }; export enum CASHFLOW_DIRECTION {