refactor(nestjs): hook up new endpoints

This commit is contained in:
Ahmed Bouhuolia
2025-05-16 01:41:11 +02:00
parent ecb80b2cf2
commit 4de1ef71ca
23 changed files with 644 additions and 38 deletions

View File

@@ -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()

View File

@@ -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],
})

View File

@@ -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

View File

@@ -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 };
}
}

View File

@@ -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);
}
}

View File

@@ -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,
);
}
}

View File

@@ -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',
}

View File

@@ -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',
},
};
};

View File

@@ -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;
}