mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat(server): categorize the synced bank transactions
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Service, Inject } from 'typedi';
|
import { Service, Inject } from 'typedi';
|
||||||
import { check } from 'express-validator';
|
import { check, oneOf } from 'express-validator';
|
||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import BaseController from '../BaseController';
|
import BaseController from '../BaseController';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
@@ -75,14 +75,16 @@ export default class NewCashflowTransactionController extends BaseController {
|
|||||||
public get categorizeCashflowTransactionValidationSchema() {
|
public get categorizeCashflowTransactionValidationSchema() {
|
||||||
return [
|
return [
|
||||||
check('date').exists().isISO8601().toDate(),
|
check('date').exists().isISO8601().toDate(),
|
||||||
|
oneOf([
|
||||||
check('to_account_id').exists().isInt().toInt(),
|
check('to_account_id').exists().isInt().toInt(),
|
||||||
check('from_account_id').exists().isInt().toInt(),
|
check('from_account_id').exists().isInt().toInt(),
|
||||||
|
]),
|
||||||
|
check('transaction_number').optional(),
|
||||||
check('transaction_type').exists(),
|
check('transaction_type').exists(),
|
||||||
check('reference_no').optional(),
|
check('reference_no').optional(),
|
||||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||||
check('description').optional(),
|
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);
|
const ownerContributionDTO = this.matchedBodyData(req);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { cashflowTransaction } =
|
const cashflowTransaction =
|
||||||
await this.newCashflowTranscationService.newCashflowTransaction(
|
await this.newCashflowTranscationService.newCashflowTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
ownerContributionDTO,
|
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);
|
next(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ exports.up = function (knex) {
|
|||||||
table.increments('id');
|
table.increments('id');
|
||||||
table.date('date').index();
|
table.date('date').index();
|
||||||
table.decimal('amount');
|
table.decimal('amount');
|
||||||
|
table.string('currency_code');
|
||||||
table.string('reference_no').index();
|
table.string('reference_no').index();
|
||||||
table
|
table
|
||||||
.integer('account_id')
|
.integer('account_id')
|
||||||
|
|||||||
@@ -22,23 +22,42 @@ export default class UncategorizedCashflowTransaction extends TenantModel {
|
|||||||
* Retrieves the withdrawal amount.
|
* Retrieves the withdrawal amount.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
public withdrawal() {
|
public get withdrawal() {
|
||||||
return this.amount > 0 ? Math.abs(this.amount) : 0;
|
return this.amount < 0 ? Math.abs(this.amount) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the deposit amount.
|
* Retrieves the deposit amount.
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
public deposit() {
|
public get deposit(): number {
|
||||||
return this.amount < 0 ? Math.abs(this.amount) : 0;
|
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.
|
* Virtual attributes.
|
||||||
*/
|
*/
|
||||||
static get virtualAttributes() {
|
static get virtualAttributes() {
|
||||||
return ['withdrawal', 'deposit'];
|
return [
|
||||||
|
'withdrawal',
|
||||||
|
'deposit',
|
||||||
|
'isDepositTransaction',
|
||||||
|
'isWithdrawalTransaction',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Knex } from 'knex';
|
|||||||
import { transformCategorizeTransToCashflow } from './utils';
|
import { transformCategorizeTransToCashflow } from './utils';
|
||||||
import { CommandCashflowValidator } from './CommandCasflowValidator';
|
import { CommandCashflowValidator } from './CommandCasflowValidator';
|
||||||
import NewCashflowTransactionService from './NewCashflowTransactionService';
|
import NewCashflowTransactionService from './NewCashflowTransactionService';
|
||||||
|
import { TransferAuthorizationGuaranteeDecision } from 'plaid';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CategorizeCashflowTransaction {
|
export class CategorizeCashflowTransaction {
|
||||||
@@ -50,6 +51,12 @@ export class CategorizeCashflowTransaction {
|
|||||||
// Validates the transaction shouldn't be categorized before.
|
// Validates the transaction shouldn't be categorized before.
|
||||||
this.commandValidators.validateTransactionShouldNotCategorized(transaction);
|
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.
|
// Edits the cashflow transaction under UOW env.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||||
// Triggers `onTransactionCategorizing` event.
|
// Triggers `onTransactionCategorizing` event.
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { includes, camelCase, upperFirst } from 'lodash';
|
import { includes, camelCase, upperFirst } from 'lodash';
|
||||||
import { IAccount } from '@/interfaces';
|
import { IAccount, IUncategorizedCashflowTransaction } from '@/interfaces';
|
||||||
import { getCashflowTransactionType } from './utils';
|
import { getCashflowTransactionType } from './utils';
|
||||||
import { ServiceError } from '@/exceptions';
|
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';
|
import CashflowTransaction from '@/models/CashflowTransaction';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@@ -71,4 +75,28 @@ export class CommandCashflowValidator {
|
|||||||
throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export const ERRORS = {
|
|||||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||||
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
|
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
|
||||||
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
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 {
|
export enum CASHFLOW_DIRECTION {
|
||||||
|
|||||||
Reference in New Issue
Block a user