mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
refactor(nestjs): hook up new endpoints
This commit is contained in:
@@ -9,7 +9,10 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { BankingTransactionsApplication } from './BankingTransactionsApplication.service';
|
||||
import { IBankAccountsFilter } from './types/BankingTransactions.types';
|
||||
import {
|
||||
IBankAccountsFilter,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
} from './types/BankingTransactions.types';
|
||||
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
|
||||
|
||||
@Controller('banking/transactions')
|
||||
@@ -19,9 +22,13 @@ export class BankingTransactionsController {
|
||||
private readonly bankingTransactionsApplication: BankingTransactionsApplication,
|
||||
) {}
|
||||
|
||||
@Get('')
|
||||
async getBankAccounts(@Query() filterDTO: IBankAccountsFilter) {
|
||||
return this.bankingTransactionsApplication.getBankAccounts(filterDTO);
|
||||
@Get()
|
||||
async getBankAccountTransactions(
|
||||
@Query() query: ICashflowAccountTransactionsQuery,
|
||||
) {
|
||||
return this.bankingTransactionsApplication.getBankAccountTransactions(
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -24,6 +24,8 @@ import { GetBankAccountsService } from './queries/GetBankAccounts.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
import { BankAccount } from './models/BankAccount';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
|
||||
import { GetBankAccountTransactionsRepository } from './queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service';
|
||||
|
||||
const models = [
|
||||
RegisterTenancyModel(UncategorizedBankTransaction),
|
||||
@@ -57,6 +59,8 @@ const models = [
|
||||
CommandBankTransactionValidator,
|
||||
BranchTransactionDTOTransformer,
|
||||
RemovePendingUncategorizedTransaction,
|
||||
GetBankAccountTransactionsRepository,
|
||||
GetBankAccountTransactionsService,
|
||||
],
|
||||
exports: [...models, RemovePendingUncategorizedTransaction],
|
||||
})
|
||||
|
||||
@@ -4,9 +4,11 @@ import { CreateBankTransactionService } from './commands/CreateBankTransaction.s
|
||||
import { GetBankTransactionService } from './queries/GetBankTransaction.service';
|
||||
import {
|
||||
IBankAccountsFilter,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
} from './types/BankingTransactions.types';
|
||||
import { GetBankAccountsService } from './queries/GetBankAccounts.service';
|
||||
import { CreateBankTransactionDto } from './dtos/CreateBankTransaction.dto';
|
||||
import { GetBankAccountTransactionsService } from './queries/GetBankAccountTransactions/GetBankAccountTransactions.service';
|
||||
|
||||
@Injectable()
|
||||
export class BankingTransactionsApplication {
|
||||
@@ -15,6 +17,7 @@ export class BankingTransactionsApplication {
|
||||
private readonly deleteTransactionService: DeleteCashflowTransaction,
|
||||
private readonly getCashflowTransactionService: GetBankTransactionService,
|
||||
private readonly getBankAccountsService: GetBankAccountsService,
|
||||
private readonly getBankAccountTransactionsService: GetBankAccountTransactionsService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -37,6 +40,16 @@ export class BankingTransactionsApplication {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bank transactions of the given bank id.
|
||||
* @param {ICashflowAccountTransactionsQuery} query
|
||||
*/
|
||||
public getBankAccountTransactions(query: ICashflowAccountTransactionsQuery) {
|
||||
return this.getBankAccountTransactionsService.bankAccountTransactions(
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves specific cashflow transaction.
|
||||
* @param {number} cashflowTransactionId
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getBankAccountTransactionsDefaultQuery } from './_utils';
|
||||
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
|
||||
import { GetBankAccountTransactions } from './GetBankAccountTransactions';
|
||||
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
|
||||
|
||||
@Injectable()
|
||||
export class GetBankAccountTransactionsService {
|
||||
constructor(
|
||||
private readonly getBankAccountTransactionsRepository: GetBankAccountTransactionsRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions report data.
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
* @return {Promise<IInvetoryItemDetailDOO>}
|
||||
*/
|
||||
public async bankAccountTransactions(
|
||||
query: ICashflowAccountTransactionsQuery,
|
||||
) {
|
||||
const parsedQuery = {
|
||||
...getBankAccountTransactionsDefaultQuery(),
|
||||
...query,
|
||||
};
|
||||
this.getBankAccountTransactionsRepository.setQuery(parsedQuery);
|
||||
|
||||
await this.getBankAccountTransactionsRepository.asyncInit();
|
||||
|
||||
// Retrieve the computed report.
|
||||
const report = new GetBankAccountTransactions(
|
||||
this.getBankAccountTransactionsRepository,
|
||||
parsedQuery,
|
||||
);
|
||||
const transactions = report.reportData();
|
||||
const pagination = this.getBankAccountTransactionsRepository.pagination;
|
||||
|
||||
return { transactions, pagination };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// @ts-nocheck
|
||||
import R from 'ramda';
|
||||
import moment from 'moment';
|
||||
import { first, isEmpty } from 'lodash';
|
||||
import {
|
||||
ICashflowAccountTransaction,
|
||||
ICashflowAccountTransactionsQuery,
|
||||
} from '../../types/BankingTransactions.types';
|
||||
import { BankTransactionStatus } from './_constants';
|
||||
import { FinancialSheet } from '@/modules/FinancialStatements/common/FinancialSheet';
|
||||
import { formatBankTransactionsStatus } from './_utils';
|
||||
import { GetBankAccountTransactionsRepository } from './GetBankAccountTransactionsRepo.service';
|
||||
import { runningBalance } from '@/utils/running-balance';
|
||||
|
||||
export class GetBankAccountTransactions extends FinancialSheet {
|
||||
private runningBalance: any;
|
||||
private query: ICashflowAccountTransactionsQuery;
|
||||
private repo: GetBankAccountTransactionsRepository;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IAccountTransaction[]} transactions -
|
||||
* @param {number} openingBalance -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
constructor(
|
||||
repo: GetBankAccountTransactionsRepository,
|
||||
query: ICashflowAccountTransactionsQuery,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.repo = repo;
|
||||
this.query = query;
|
||||
this.runningBalance = runningBalance(this.repo.openingBalance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the transaction status.
|
||||
* @param {} transaction
|
||||
* @returns {BankTransactionStatus}
|
||||
*/
|
||||
private getTransactionStatus(transaction: any): BankTransactionStatus {
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
);
|
||||
if (!isEmpty(categorizedTrans)) {
|
||||
return BankTransactionStatus.Categorized;
|
||||
} else if (!isEmpty(matchedTrans)) {
|
||||
return BankTransactionStatus.Matched;
|
||||
} else {
|
||||
return BankTransactionStatus.Manual;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the uncategoized transaction id from the given transaction.
|
||||
* @param transaction
|
||||
* @returns {number|null}
|
||||
*/
|
||||
private getUncategorizedTransId(transaction: any): number {
|
||||
// The given transaction would be categorized, matched or not, so we'd take a look at
|
||||
// the categorized transaction first to get the id if not exist, then should look at the matched
|
||||
// transaction if not exist too, so the given transaction has no uncategorized transaction id.
|
||||
const categorizedTrans = this.repo.uncategorizedTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
);
|
||||
const matchedTrans = this.repo.matchedBankTransactionsMapByRef.get(
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
);
|
||||
// Relation between the transaction and matching always been one-to-one.
|
||||
const firstCategorizedTrans = first(categorizedTrans);
|
||||
const firstMatchedTrans = first(matchedTrans);
|
||||
|
||||
return (
|
||||
firstCategorizedTrans?.id ||
|
||||
firstMatchedTrans?.uncategorizedTransactionId ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*Transformes the account transaction to to cashflow transaction node.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionNode = (transaction: any): ICashflowAccountTransaction => {
|
||||
const status = this.getTransactionStatus(transaction);
|
||||
const uncategorizedTransactionId =
|
||||
this.getUncategorizedTransId(transaction);
|
||||
|
||||
return {
|
||||
date: transaction.date,
|
||||
formattedDate: moment(transaction.date).format('YYYY-MM-DD'),
|
||||
|
||||
withdrawal: transaction.credit,
|
||||
deposit: transaction.debit,
|
||||
|
||||
formattedDeposit: this.formatNumber(transaction.debit),
|
||||
formattedWithdrawal: this.formatNumber(transaction.credit),
|
||||
|
||||
referenceId: transaction.referenceId,
|
||||
referenceType: transaction.referenceType,
|
||||
|
||||
formattedTransactionType: transaction.referenceTypeFormatted,
|
||||
|
||||
transactionNumber: transaction.transactionNumber,
|
||||
referenceNumber: transaction.referenceNumber,
|
||||
|
||||
runningBalance: this.runningBalance.amount(),
|
||||
formattedRunningBalance: this.formatNumber(this.runningBalance.amount()),
|
||||
|
||||
balance: 0,
|
||||
formattedBalance: '',
|
||||
status,
|
||||
formattedStatus: formatBankTransactionsStatus(status),
|
||||
uncategorizedTransactionId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Associate cashflow transaction node with running balance attribute.
|
||||
* @param {IAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionRunningBalance = (
|
||||
transaction: ICashflowAccountTransaction,
|
||||
): ICashflowAccountTransaction => {
|
||||
const amount = transaction.deposit - transaction.withdrawal;
|
||||
|
||||
const biggerThanZero = R.lt(0, amount);
|
||||
const lowerThanZero = R.gt(0, amount);
|
||||
|
||||
const absAmount = Math.abs(amount);
|
||||
|
||||
R.when(R.always(biggerThanZero), this.runningBalance.decrement)(absAmount);
|
||||
R.when(R.always(lowerThanZero), this.runningBalance.increment)(absAmount);
|
||||
|
||||
const runningBalance = this.runningBalance.amount();
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
runningBalance,
|
||||
formattedRunningBalance: this.formatNumber(runningBalance),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Associate to balance attribute to cashflow transaction node.
|
||||
* @param {ICashflowAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionBalance = (
|
||||
transaction: ICashflowAccountTransaction,
|
||||
): ICashflowAccountTransaction => {
|
||||
const balance =
|
||||
transaction.runningBalance +
|
||||
transaction.withdrawal * -1 +
|
||||
transaction.deposit;
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
balance,
|
||||
formattedBalance: this.formatNumber(balance),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transformes the given account transaction to cashflow report transaction.
|
||||
* @param {ICashflowAccountTransaction} transaction
|
||||
* @returns {ICashflowAccountTransaction}
|
||||
*/
|
||||
private transactionTransformer = (
|
||||
transaction,
|
||||
): ICashflowAccountTransaction => {
|
||||
return R.compose(
|
||||
this.transactionBalance,
|
||||
this.transactionRunningBalance,
|
||||
this.transactionNode,
|
||||
)(transaction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the report transactions node.
|
||||
* @param {} transactions
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
private transactionsNode = (transactions): ICashflowAccountTransaction[] => {
|
||||
return R.map(this.transactionTransformer)(transactions);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the reprot data node.
|
||||
* @returns {ICashflowAccountTransaction[]}
|
||||
*/
|
||||
public reportData(): ICashflowAccountTransaction[] {
|
||||
return this.transactionsNode(this.repo.transactions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Injectable, Scope } from '@nestjs/common';
|
||||
import { ICashflowAccountTransactionsQuery } from '../../types/BankingTransactions.types';
|
||||
import {
|
||||
groupMatchedBankTransactions,
|
||||
groupUncategorizedTransactions,
|
||||
} from './_utils';
|
||||
|
||||
@Injectable({ scope: Scope.REQUEST })
|
||||
export class GetBankAccountTransactionsRepository {
|
||||
private models: any;
|
||||
public query: ICashflowAccountTransactionsQuery;
|
||||
public transactions: any;
|
||||
public uncategorizedTransactions: any;
|
||||
public uncategorizedTransactionsMapByRef: Map<string, any>;
|
||||
public matchedBankTransactions: any;
|
||||
public matchedBankTransactionsMapByRef: Map<string, any>;
|
||||
public pagination: any;
|
||||
public openingBalance: any;
|
||||
|
||||
setQuery(query: ICashflowAccountTransactionsQuery) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async initalize the resources.
|
||||
*/
|
||||
async asyncInit() {
|
||||
await this.initCashflowAccountTransactions();
|
||||
await this.initCashflowAccountOpeningBalance();
|
||||
await this.initCategorizedTransactions();
|
||||
await this.initMatchedTransactions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICashflowAccountTransactionsQuery} query -
|
||||
*/
|
||||
async initCashflowAccountTransactions() {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
const { results, pagination } = await AccountTransaction.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.pagination(this.query.page - 1, this.query.pageSize);
|
||||
|
||||
this.transactions = results;
|
||||
this.pagination = pagination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account opening balance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} accountId
|
||||
* @param {IPaginationMeta} pagination
|
||||
* @return {Promise<number>}
|
||||
*/
|
||||
async initCashflowAccountOpeningBalance(): Promise<void> {
|
||||
const { AccountTransaction } = this.models;
|
||||
|
||||
// Retrieve the opening balance of credit and debit balances.
|
||||
const openingBalancesSubquery = AccountTransaction.query()
|
||||
.where('account_id', this.query.accountId)
|
||||
.orderBy([
|
||||
{ column: 'date', order: 'desc' },
|
||||
{ column: 'created_at', order: 'desc' },
|
||||
])
|
||||
.limit(this.pagination.total)
|
||||
.offset(this.pagination.pageSize * (this.pagination.page - 1));
|
||||
|
||||
// Sumation of credit and debit balance.
|
||||
const openingBalances = await AccountTransaction.query()
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit')
|
||||
.from(openingBalancesSubquery.as('T'))
|
||||
.first();
|
||||
|
||||
const openingBalance = openingBalances.debit - openingBalances.credit;
|
||||
|
||||
this.openingBalance = openingBalance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the uncategorized transactions of the bank account.
|
||||
*/
|
||||
async initCategorizedTransactions() {
|
||||
const { UncategorizedCashflowTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const uncategorizedTransactions =
|
||||
await UncategorizedCashflowTransaction.query().whereIn(
|
||||
['categorizeRefType', 'categorizeRefId'],
|
||||
refs,
|
||||
);
|
||||
|
||||
this.uncategorizedTransactions = uncategorizedTransactions;
|
||||
this.uncategorizedTransactionsMapByRef = groupUncategorizedTransactions(
|
||||
uncategorizedTransactions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the matched bank transactions of the bank account.
|
||||
*/
|
||||
async initMatchedTransactions(): Promise<void> {
|
||||
const { MatchedBankTransaction } = this.models;
|
||||
const refs = this.transactions.map((t) => [t.referenceType, t.referenceId]);
|
||||
|
||||
const matchedBankTransactions =
|
||||
await MatchedBankTransaction.query().whereIn(
|
||||
['referenceType', 'referenceId'],
|
||||
refs,
|
||||
);
|
||||
this.matchedBankTransactions = matchedBankTransactions;
|
||||
this.matchedBankTransactionsMapByRef = groupMatchedBankTransactions(
|
||||
matchedBankTransactions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const ERRORS = {
|
||||
ACCOUNT_ID_HAS_INVALID_TYPE: 'ACCOUNT_ID_HAS_INVALID_TYPE',
|
||||
};
|
||||
|
||||
export enum BankTransactionStatus {
|
||||
Categorized = 'categorized',
|
||||
Matched = 'matched',
|
||||
Manual = 'manual',
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as R from 'ramda';
|
||||
|
||||
export const groupUncategorizedTransactions = (
|
||||
uncategorizedTransactions: any,
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction: any) =>
|
||||
`${transaction.categorizeRefType}-${transaction.categorizeRefId}`,
|
||||
uncategorizedTransactions,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const groupMatchedBankTransactions = (
|
||||
uncategorizedTransactions: any,
|
||||
): Map<string, any> => {
|
||||
return new Map(
|
||||
R.toPairs(
|
||||
R.groupBy(
|
||||
(transaction: any) =>
|
||||
`${transaction.referenceType}-${transaction.referenceId}`,
|
||||
uncategorizedTransactions,
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export const formatBankTransactionsStatus = (status) => {
|
||||
switch (status) {
|
||||
case 'categorized':
|
||||
return 'Categorized';
|
||||
case 'matched':
|
||||
return 'Matched';
|
||||
case 'manual':
|
||||
return 'Manual';
|
||||
}
|
||||
};
|
||||
|
||||
export const getBankAccountTransactionsDefaultQuery = () => {
|
||||
return {
|
||||
pageSize: 50,
|
||||
page: 1,
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
showZero: false,
|
||||
formatMoney: 'total',
|
||||
negativeFormat: 'mines',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { Knex } from 'knex';
|
||||
import { UncategorizedBankTransaction } from '../models/UncategorizedBankTransaction';
|
||||
import { BankTransaction } from '../models/BankTransaction';
|
||||
import { CreateBankTransactionDto } from '../dtos/CreateBankTransaction.dto';
|
||||
import { INumberFormatQuery } from '@/modules/FinancialStatements/types/Report.types';
|
||||
|
||||
export interface IPendingTransactionRemovingEventPayload {
|
||||
uncategorizedTransactionId: number;
|
||||
@@ -128,3 +129,39 @@ export interface IGetUncategorizedTransactionsQuery {
|
||||
minAmount?: number;
|
||||
maxAmount?: number;
|
||||
}
|
||||
|
||||
export interface ICashflowAccountTransactionsQuery {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
accountId: number;
|
||||
numberFormat: INumberFormatQuery;
|
||||
}
|
||||
|
||||
export interface ICashflowAccountTransaction {
|
||||
withdrawal: number;
|
||||
deposit: number;
|
||||
runningBalance: number;
|
||||
|
||||
formattedWithdrawal: string;
|
||||
formattedDeposit: string;
|
||||
formattedRunningBalance: string;
|
||||
|
||||
transactionNumber: string;
|
||||
referenceNumber: string;
|
||||
|
||||
referenceId: number;
|
||||
referenceType: string;
|
||||
|
||||
formattedTransactionType: string;
|
||||
|
||||
balance: number;
|
||||
formattedBalance: string;
|
||||
|
||||
date: Date;
|
||||
formattedDate: string;
|
||||
|
||||
status: string;
|
||||
formattedStatus: string;
|
||||
|
||||
uncategorizedTransactionId: number;
|
||||
}
|
||||
Reference in New Issue
Block a user