diff --git a/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts b/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts index 8184a65f2..bf9fa8c51 100644 --- a/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts +++ b/packages/server/src/modules/BankingAccounts/BankAccounts.controller.ts @@ -1,12 +1,25 @@ -import { Controller, Param, Post } from '@nestjs/common'; +import { Controller, Get, Param, Post, Query } from '@nestjs/common'; import { BankAccountsApplication } from './BankAccountsApplication.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ICashflowAccountsFilter } from './types/BankAccounts.types'; @Controller('banking/accounts') @ApiTags('banking-accounts') export class BankAccountsController { constructor(private bankAccountsApplication: BankAccountsApplication) {} + @Get() + @ApiOperation({ summary: 'Retrieve the bank accounts.' }) + getBankAccounts(@Query() filterDto: ICashflowAccountsFilter) { + return this.bankAccountsApplication.getBankAccounts(filterDto); + } + + @Get(':bankAccountId/summary') + @ApiOperation({ summary: 'Retrieve the bank account summary.' }) + getBankAccountSummary(@Param('bankAccountId') bankAccountId: number) { + return this.bankAccountsApplication.getBankAccountSumnmary(bankAccountId); + } + @Post(':id/disconnect') @ApiOperation({ summary: 'Disconnect the bank connection of the given bank account.', diff --git a/packages/server/src/modules/BankingAccounts/BankAccounts.module.ts b/packages/server/src/modules/BankingAccounts/BankAccounts.module.ts index df8c601af..32f9e4a8f 100644 --- a/packages/server/src/modules/BankingAccounts/BankAccounts.module.ts +++ b/packages/server/src/modules/BankingAccounts/BankAccounts.module.ts @@ -12,6 +12,9 @@ import { PlaidModule } from '../Plaid/Plaid.module'; import { BankRulesModule } from '../BankRules/BankRules.module'; import { BankingTransactionsRegonizeModule } from '../BankingTranasctionsRegonize/BankingTransactionsRegonize.module'; import { BankingTransactionsModule } from '../BankingTransactions/BankingTransactions.module'; +import { GetBankAccountsService } from './queries/GetBankAccounts'; +import { DynamicListModule } from '../DynamicListing/DynamicList.module'; +import { GetBankAccountSummary } from './queries/GetBankAccountSummary'; @Module({ imports: [ @@ -20,6 +23,7 @@ import { BankingTransactionsModule } from '../BankingTransactions/BankingTransac BankRulesModule, BankingTransactionsRegonizeModule, BankingTransactionsModule, + DynamicListModule ], providers: [ DisconnectBankAccountService, @@ -29,6 +33,8 @@ import { BankingTransactionsModule } from '../BankingTransactions/BankingTransac DeleteUncategorizedTransactionsOnAccountDeleting, DisconnectPlaidItemOnAccountDeleted, BankAccountsApplication, + GetBankAccountsService, + GetBankAccountSummary ], exports: [BankAccountsApplication], controllers: [BankAccountsController], diff --git a/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts b/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts index c7e1a757a..f53f7fd64 100644 --- a/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts +++ b/packages/server/src/modules/BankingAccounts/BankAccountsApplication.service.ts @@ -3,16 +3,39 @@ import { DisconnectBankAccountService } from './commands/DisconnectBankAccount.s import { RefreshBankAccountService } from './commands/RefreshBankAccount.service'; import { ResumeBankAccountFeedsService } from './commands/ResumeBankAccountFeeds.service'; import { PauseBankAccountFeeds } from './commands/PauseBankAccountFeeds.service'; +import { GetBankAccountsService } from './queries/GetBankAccounts'; +import { ICashflowAccountsFilter } from './types/BankAccounts.types'; +import { GetBankAccountSummary } from './queries/GetBankAccountSummary'; @Injectable() export class BankAccountsApplication { constructor( - private disconnectBankAccountService: DisconnectBankAccountService, + private readonly getBankAccountsService: GetBankAccountsService, + private readonly getBankAccountSummaryService: GetBankAccountSummary, + private readonly disconnectBankAccountService: DisconnectBankAccountService, private readonly refreshBankAccountService: RefreshBankAccountService, private readonly resumeBankAccountFeedsService: ResumeBankAccountFeedsService, private readonly pauseBankAccountFeedsService: PauseBankAccountFeeds, ) {} + /** + * Retrieves the bank accounts. + * @param {ICashflowAccountsFilter} filterDto - + */ + getBankAccounts(filterDto: ICashflowAccountsFilter) { + return this.getBankAccountsService.getCashflowAccounts(filterDto); + } + + /** + * Retrieves the given bank account summary. + * @param {number} bankAccountId + */ + getBankAccountSumnmary(bankAccountId: number) { + return this.getBankAccountSummaryService.getBankAccountSummary( + bankAccountId, + ); + } + /** * Disconnects the given bank account. * @param {number} bankAccountId - Bank account identifier. diff --git a/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts b/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts new file mode 100644 index 000000000..74d2052d6 --- /dev/null +++ b/packages/server/src/modules/BankingAccounts/queries/GetBankAccounts.ts @@ -0,0 +1,61 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ACCOUNT_TYPE } from '@/constants/accounts'; +import { Account } from '@/modules/Accounts/models/Account.model'; +import { CashflowAccountTransformer } from '@/modules/BankingTransactions/queries/BankAccountTransformer'; +import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; +import { ICashflowAccountsFilter } from '../types/BankAccounts.types'; +import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service'; +import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service'; + +@Injectable() +export class GetBankAccountsService { + constructor( + private readonly dynamicListService: DynamicListService, + private readonly transformer: TransformerInjectable, + + @Inject(Account.name) + private readonly accountModel: TenantModelProxy, + ) {} + + /** + * Retrieve the cash flow accounts. + * @param {ICashflowAccountsFilter} filterDTO - Filter DTO. + * @returns {ICashflowAccount[]} + */ + public async getCashflowAccounts(filterDTO: ICashflowAccountsFilter) { + const _filterDto = { + sortOrder: 'desc', + columnSortBy: 'created_at', + inactiveMode: false, + ...filterDTO, + }; + // Parsees accounts list filter DTO. + const filter = this.dynamicListService.parseStringifiedFilter(_filterDto); + + // Dynamic list service. + const dynamicList = await this.dynamicListService.dynamicList( + this.accountModel(), + filter, + ); + // Retrieve accounts model based on the given query. + const accounts = await this.accountModel() + .query() + .onBuild((builder) => { + dynamicList.buildQuery()(builder); + + builder.whereIn('account_type', [ + ACCOUNT_TYPE.BANK, + ACCOUNT_TYPE.CASH, + ACCOUNT_TYPE.CREDIT_CARD, + ]); + builder.modify('inactiveMode', filter.inactiveMode); + }); + // Retrieves the transformed accounts. + const transformed = await this.transformer.transform( + accounts, + new CashflowAccountTransformer(), + ); + + return transformed; + } +} diff --git a/packages/server/src/modules/BankingAccounts/types/BankAccounts.types.ts b/packages/server/src/modules/BankingAccounts/types/BankAccounts.types.ts index 951150d11..d607334a4 100644 --- a/packages/server/src/modules/BankingAccounts/types/BankAccounts.types.ts +++ b/packages/server/src/modules/BankingAccounts/types/BankAccounts.types.ts @@ -1,3 +1,4 @@ +import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; import { Knex } from 'knex'; export interface IBankAccountDisconnectingEventPayload { @@ -15,3 +16,11 @@ export const ERRORS = { BANK_ACCOUNT_FEEDS_ALREADY_PAUSED: 'BANK_ACCOUNT_FEEDS_ALREADY_PAUSED', BANK_ACCOUNT_FEEDS_ALREADY_RESUMED: 'BANK_ACCOUNT_FEEDS_ALREADY_RESUMED', }; + + +export interface ICashflowAccountsFilter extends IDynamicListFilter{ + page: number; + pageSize: number; + inactiveMode: boolean; + viewSlug?: string; +} \ No newline at end of file diff --git a/packages/server/src/modules/BankingTransactions/BankingTransactions.controller.ts b/packages/server/src/modules/BankingTransactions/BankingTransactions.controller.ts index fc1ba8c83..54f4698f7 100644 --- a/packages/server/src/modules/BankingTransactions/BankingTransactions.controller.ts +++ b/packages/server/src/modules/BankingTransactions/BankingTransactions.controller.ts @@ -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() diff --git a/packages/server/src/modules/BankingTransactions/BankingTransactions.module.ts b/packages/server/src/modules/BankingTransactions/BankingTransactions.module.ts index d50193fd4..38d046fc9 100644 --- a/packages/server/src/modules/BankingTransactions/BankingTransactions.module.ts +++ b/packages/server/src/modules/BankingTransactions/BankingTransactions.module.ts @@ -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], }) diff --git a/packages/server/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts b/packages/server/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts index 16168675d..fbaeaca3f 100644 --- a/packages/server/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts +++ b/packages/server/src/modules/BankingTransactions/BankingTransactionsApplication.service.ts @@ -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 diff --git a/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.service.ts b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.service.ts new file mode 100644 index 000000000..6af6a4338 --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.service.ts @@ -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} + */ + 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 }; + } +} diff --git a/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.ts b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.ts new file mode 100644 index 000000000..0741918c0 --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactions.ts @@ -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); + } +} diff --git a/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service.ts b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service.ts new file mode 100644 index 000000000..c9a934d05 --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/GetBankAccountTransactionsRepo.service.ts @@ -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; + public matchedBankTransactions: any; + public matchedBankTransactionsMapByRef: Map; + 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} + */ + async initCashflowAccountOpeningBalance(): Promise { + 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 { + 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, + ); + } +} diff --git a/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_constants.ts b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_constants.ts new file mode 100644 index 000000000..478ae88ee --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_constants.ts @@ -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', +} \ No newline at end of file diff --git a/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_utils.ts b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_utils.ts new file mode 100644 index 000000000..7c8c055ab --- /dev/null +++ b/packages/server/src/modules/BankingTransactions/queries/GetBankAccountTransactions/_utils.ts @@ -0,0 +1,54 @@ +import * as R from 'ramda'; + +export const groupUncategorizedTransactions = ( + uncategorizedTransactions: any, +): Map => { + return new Map( + R.toPairs( + R.groupBy( + (transaction: any) => + `${transaction.categorizeRefType}-${transaction.categorizeRefId}`, + uncategorizedTransactions, + ), + ), + ); +}; + +export const groupMatchedBankTransactions = ( + uncategorizedTransactions: any, +): Map => { + 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', + }, + }; +}; diff --git a/packages/server/src/modules/BankingTransactions/types/BankingTransactions.types.ts b/packages/server/src/modules/BankingTransactions/types/BankingTransactions.types.ts index 8be092c84..9e611b8fa 100644 --- a/packages/server/src/modules/BankingTransactions/types/BankingTransactions.types.ts +++ b/packages/server/src/modules/BankingTransactions/types/BankingTransactions.types.ts @@ -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; +} \ No newline at end of file diff --git a/packages/server/src/modules/ItemCategories/commands/EditItemCategory.service.ts b/packages/server/src/modules/ItemCategories/commands/EditItemCategory.service.ts index 4784a5eab..ac8c5e2c2 100644 --- a/packages/server/src/modules/ItemCategories/commands/EditItemCategory.service.ts +++ b/packages/server/src/modules/ItemCategories/commands/EditItemCategory.service.ts @@ -1,3 +1,5 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { Knex } from 'knex'; import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service'; import { CommandItemCategoryValidatorService } from './CommandItemCategoryValidator.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -7,13 +9,12 @@ import { IItemCategoryOTD, } from '../ItemCategory.interfaces'; import { SystemUser } from '@/modules/System/models/SystemUser'; -import { Knex } from 'knex'; import { ItemCategory } from '../models/ItemCategory.model'; -import { Inject } from '@nestjs/common'; import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { EditItemCategoryDto } from '../dtos/ItemCategory.dto'; +@Injectable() export class EditItemCategoryService { /** * @param {UnitOfWork} uow - Unit of work. diff --git a/packages/server/src/modules/ItemCategories/dtos/ItemCategory.dto.ts b/packages/server/src/modules/ItemCategories/dtos/ItemCategory.dto.ts index f3b87afe9..83878d71b 100644 --- a/packages/server/src/modules/ItemCategories/dtos/ItemCategory.dto.ts +++ b/packages/server/src/modules/ItemCategories/dtos/ItemCategory.dto.ts @@ -17,14 +17,6 @@ class CommandItemCategoryDto { }) description?: string; - @IsNumber() - @IsNotEmpty() - @ApiProperty({ - example: 1, - description: 'The user ID', - }) - userId: number; - @IsNumber() @IsOptional() @ApiProperty({ example: 1, description: 'The cost account ID' }) diff --git a/packages/server/src/utils/running-balance.ts b/packages/server/src/utils/running-balance.ts new file mode 100644 index 000000000..690c23c76 --- /dev/null +++ b/packages/server/src/utils/running-balance.ts @@ -0,0 +1,15 @@ + + +export const runningBalance = (amount: number) => { + let runningBalance = amount; + + return { + decrement: (decrement: number) => { + runningBalance -= decrement; + }, + increment: (increment: number) => { + runningBalance += increment; + }, + amount: () => runningBalance, + }; +}; \ No newline at end of file diff --git a/packages/webapp/src/hooks/query/accounts.tsx b/packages/webapp/src/hooks/query/accounts.tsx index e7c74a868..544062bce 100644 --- a/packages/webapp/src/hooks/query/accounts.tsx +++ b/packages/webapp/src/hooks/query/accounts.tsx @@ -6,7 +6,7 @@ import t from './types'; // Transform the account. const transformAccount = (response) => { - return response.data.account; + return response.data; }; const commonInvalidateQueries = (query) => { @@ -58,9 +58,9 @@ export function useAccount(id, props) { export function useAccountsTypes(props) { return useRequestQuery( [t.ACCOUNTS_TYPES], - { method: 'get', url: 'account_types' }, + { method: 'get', url: 'accounts/types' }, { - select: (res) => res.data.account_types, + select: (res) => res.data, defaultData: [], ...props, }, diff --git a/packages/webapp/src/hooks/query/cashflowAccounts.tsx b/packages/webapp/src/hooks/query/cashflowAccounts.tsx index 23ad4fd7b..07d16a4dc 100644 --- a/packages/webapp/src/hooks/query/cashflowAccounts.tsx +++ b/packages/webapp/src/hooks/query/cashflowAccounts.tsx @@ -39,7 +39,7 @@ export function useCashflowAccounts(query, props) { [t.CASH_FLOW_ACCOUNTS, query], { method: 'get', url: 'banking/accounts', params: query }, { - select: (res) => res.data.cashflow_accounts, + select: (res) => res.data, defaultData: [], ...props, }, diff --git a/packages/webapp/src/hooks/query/customers.tsx b/packages/webapp/src/hooks/query/customers.tsx index 771a49926..139a8fc60 100644 --- a/packages/webapp/src/hooks/query/customers.tsx +++ b/packages/webapp/src/hooks/query/customers.tsx @@ -124,7 +124,7 @@ export function useCustomer(id, props) { [t.CUSTOMER, id], { method: 'get', url: `customers/${id}` }, { - select: (res) => res.data.customer, + select: (res) => res.data, defaultData: {}, ...props, }, diff --git a/packages/webapp/src/hooks/query/estimates.tsx b/packages/webapp/src/hooks/query/estimates.tsx index c0bd3d867..d0aac3639 100644 --- a/packages/webapp/src/hooks/query/estimates.tsx +++ b/packages/webapp/src/hooks/query/estimates.tsx @@ -69,7 +69,7 @@ export function useEstimate(id, props) { [t.SALE_ESTIMATE, id], { method: 'get', url: `sale-estimates/${id}` }, { - select: (res) => res.data.estimate, + select: (res) => res.data, defaultData: {}, ...props, }, diff --git a/packages/webapp/src/hooks/query/items.tsx b/packages/webapp/src/hooks/query/items.tsx index 0f303cc74..06f9c3f67 100644 --- a/packages/webapp/src/hooks/query/items.tsx +++ b/packages/webapp/src/hooks/query/items.tsx @@ -167,7 +167,7 @@ export function useItem(id, props) { url: `items/${id}`, }, { - select: (response) => response.data.item, + select: (response) => response.data, defaultData: {}, ...props, }, @@ -179,10 +179,10 @@ export function useItemAssociatedInvoiceTransactions(id, props) { [t.ITEM_ASSOCIATED_WITH_INVOICES, id], { method: 'get', - url: `items/${id}/transactions/invoices`, + url: `items/${id}/invoices`, }, { - select: (res) => res.data.data, + select: (res) => res.data, defaultData: [], ...props, }, @@ -194,10 +194,10 @@ export function useItemAssociatedEstimateTransactions(id, props) { [t.ITEM_ASSOCIATED_WITH_ESTIMATES, id], { method: 'get', - url: `items/${id}/transactions/estimates`, + url: `items/${id}/estimates`, }, { - select: (res) => res.data.data, + select: (res) => res.data, defaultData: [], ...props, }, @@ -209,10 +209,10 @@ export function useItemAssociatedReceiptTransactions(id, props) { [t.ITEM_ASSOCIATED_WITH_RECEIPTS, id], { method: 'get', - url: `items/${id}/transactions/receipts`, + url: `items/${id}/receipts`, }, { - select: (res) => res.data.data, + select: (res) => res.data, defaultData: [], ...props, }, @@ -223,10 +223,10 @@ export function useItemAssociatedBillTransactions(id, props) { [t.ITEMS_ASSOCIATED_WITH_BILLS, id], { method: 'get', - url: `items/${id}/transactions/bills`, + url: `items/${id}/bills`, }, { - select: (res) => res.data.data, + select: (res) => res.data, defaultData: [], ...props, }, @@ -249,11 +249,11 @@ export function useItemWarehouseLocation(id, props) { } /** - * - * @param {*} id - * @param {*} query - * @param {*} props - * @returns + * + * @param {*} id + * @param {*} query + * @param {*} props + * @returns */ export function useItemInventoryCost(query, props) { return useRequestQuery( @@ -268,5 +268,5 @@ export function useItemInventoryCost(query, props) { defaultData: [], ...props, }, - ); + ); } diff --git a/packages/webapp/src/hooks/query/vendors.tsx b/packages/webapp/src/hooks/query/vendors.tsx index be67edd2e..b89c71c5b 100644 --- a/packages/webapp/src/hooks/query/vendors.tsx +++ b/packages/webapp/src/hooks/query/vendors.tsx @@ -112,7 +112,7 @@ export function useVendor(id, props) { [t.VENDOR, id], { method: 'get', url: `vendors/${id}` }, { - select: (res) => res.data.vendor, + select: (res) => res.data, defaultData: {}, ...props, },