From 9ae5644af997339eeb60082f09910337afeb9ed4 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 11 Aug 2024 16:14:13 +0200 Subject: [PATCH 1/4] feat: Pending bank transactions --- .../Banking/BankAccountsController.ts | 60 +++++++++++---- ...umn_to_uncategorized_transactions_table.js | 13 ++++ packages/server/src/interfaces/CashFlow.ts | 2 + .../UncategorizedCashflowTransaction.ts | 26 ++++++- .../BankAccounts/GetBankAccountSummary.ts | 15 +++- .../src/services/Banking/Plaid/PlaidSyncDB.ts | 24 +++--- .../Banking/Plaid/PlaidUpdateTransactions.ts | 6 ++ .../src/services/Banking/Plaid/utils.ts | 6 +- .../GetPendingBankAccountTransaction.ts | 53 ++++++++++++++ ...endingBankAccountTransactionTransformer.ts | 73 +++++++++++++++++++ .../RemovePendingUncategorizedTransaction.ts | 58 +++++++++++++++ .../server/src/services/Cashflow/constants.ts | 8 +- packages/server/src/subscribers/events.ts | 3 + .../src/constants/query-keys/banking.ts | 1 + .../AccountTransactionsFilterTabs.tsx | 3 +- .../AccountTransactionsUncategorizeFilter.tsx | 56 +++++++++----- .../AllTransactionsUncategorized.tsx | 2 + .../PendingTransactionsTable.tsx | 0 .../PendingTransactionsTableBoot.tsx | 0 .../src/hooks/query/bank-transaction.ts | 28 +++++++ 20 files changed, 385 insertions(+), 52 deletions(-) create mode 100644 packages/server/src/database/migrations/20240811121028_add_pending_column_to_uncategorized_transactions_table.js create mode 100644 packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts create mode 100644 packages/server/src/services/Cashflow/GetPendingBankAccountTransactionTransformer.ts create mode 100644 packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx create mode 100644 packages/webapp/src/hooks/query/bank-transaction.ts diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index 424c28857..7f134fd75 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -1,9 +1,10 @@ import { Inject, Service } from 'typedi'; import { NextFunction, Request, Response, Router } from 'express'; +import { param, query } from 'express-validator'; import BaseController from '@/api/controllers/BaseController'; import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary'; import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication'; -import { param } from 'express-validator'; +import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction'; @Service() export class BankAccountsController extends BaseController { @@ -13,6 +14,9 @@ export class BankAccountsController extends BaseController { @Inject() private bankAccountsApp: BankAccountsApplication; + @Inject() + private getPendingTransactionsService: GetPendingBankAccountTransactions; + /** * Router constructor. */ @@ -20,6 +24,16 @@ export class BankAccountsController extends BaseController { const router = Router(); router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this)); + router.get( + '/pending_transactions', + [ + query('account_id').optional().isNumeric().toInt(), + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ], + this.validationResult, + this.getBankAccountsPendingTransactions.bind(this) + ); router.post( '/:bankAccountId/disconnect', this.disconnectBankAccount.bind(this) @@ -27,17 +41,13 @@ export class BankAccountsController extends BaseController { router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this)); router.post( '/:bankAccountId/pause_feeds', - [ - param('bankAccountId').exists().isNumeric().toInt(), - ], + [param('bankAccountId').exists().isNumeric().toInt()], this.validationResult, this.pauseBankAccountFeeds.bind(this) ); router.post( '/:bankAccountId/resume_feeds', - [ - param('bankAccountId').exists().isNumeric().toInt(), - ], + [param('bankAccountId').exists().isNumeric().toInt()], this.validationResult, this.resumeBankAccountFeeds.bind(this) ); @@ -72,6 +82,30 @@ export class BankAccountsController extends BaseController { } } + /** + * Retrieves the bank account pending transactions. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async getBankAccountsPendingTransactions( + req: Request, + res: Response, + next: NextFunction + ) { + const { tenantId } = req; + + try { + const data = + await this.getPendingTransactionsService.getPendingTransactions( + tenantId + ); + return res.status(200).send(data); + } catch (error) { + next(error); + } + } + /** * Disonnect the given bank account. * @param {Request} req @@ -128,9 +162,9 @@ export class BankAccountsController extends BaseController { /** * Resumes the bank account feeds sync. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next * @returns {Promise} */ async resumeBankAccountFeeds( @@ -155,9 +189,9 @@ export class BankAccountsController extends BaseController { /** * Pauses the bank account feeds sync. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next * @returns {Promise} */ async pauseBankAccountFeeds( diff --git a/packages/server/src/database/migrations/20240811121028_add_pending_column_to_uncategorized_transactions_table.js b/packages/server/src/database/migrations/20240811121028_add_pending_column_to_uncategorized_transactions_table.js new file mode 100644 index 000000000..73713b98b --- /dev/null +++ b/packages/server/src/database/migrations/20240811121028_add_pending_column_to_uncategorized_transactions_table.js @@ -0,0 +1,13 @@ +exports.up = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.boolean('pending').defaultTo(false); + table.string('pending_plaid_transaction_id').nullable(); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.dropColumn('pending'); + table.dropColumn('pending_plaid_transaction_id'); + }); +}; diff --git a/packages/server/src/interfaces/CashFlow.ts b/packages/server/src/interfaces/CashFlow.ts index 4fe1e90c1..9e1d4bfd5 100644 --- a/packages/server/src/interfaces/CashFlow.ts +++ b/packages/server/src/interfaces/CashFlow.ts @@ -268,6 +268,8 @@ export interface CreateUncategorizedTransactionDTO { description?: string; referenceNo?: string | null; plaidTransactionId?: string | null; + pending?: boolean; + pendingPlaidTransactionId?: string | null; batch?: string; } diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index 218c35982..fbb737ccb 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -21,6 +21,7 @@ export default class UncategorizedCashflowTransaction extends mixin( plaidTransactionId!: string; recognizedTransactionId!: number; excludedAt: Date; + pending: boolean; /** * Table name. @@ -46,7 +47,8 @@ export default class UncategorizedCashflowTransaction extends mixin( 'isDepositTransaction', 'isWithdrawalTransaction', 'isRecognized', - 'isExcluded' + 'isExcluded', + 'isPending', ]; } @@ -99,6 +101,14 @@ export default class UncategorizedCashflowTransaction extends mixin( return !!this.excludedAt; } + /** + * Detarmines whether the transaction is pending. + * @returns {boolean} + */ + public get isPending(): boolean { + return !!this.pending; + } + /** * Model modifiers. */ @@ -143,6 +153,20 @@ export default class UncategorizedCashflowTransaction extends mixin( query.whereNull('categorizeRefType'); query.whereNull('categorizeRefId'); }, + + /** + * Filters the not pending transactions. + */ + notPending(query) { + query.where('pending', false); + }, + + /** + * Filters the pending transactions. + */ + pending(query) { + query.where('pending', true); + }, }; } diff --git a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts index 29c68abb6..6c66c2a5e 100644 --- a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts +++ b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts @@ -69,12 +69,22 @@ export class GetBankAccountSummary { q.count('uncategorized_cashflow_transactions.id as total'); q.first(); }); - + // Retrieves excluded transactions count. const excludedTransactionsCount = await UncategorizedCashflowTransaction.query().onBuild((q) => { q.where('accountId', bankAccountId); q.modify('excluded'); + // Count the results. + q.count('uncategorized_cashflow_transactions.id as total'); + q.first(); + }); + // Retrieves the pending transactions count. + const pendingTransactionsCount = + await UncategorizedCashflowTransaction.query().onBuild((q) => { + q.where('accountId', bankAccountId); + q.modify('pending'); + // Count the results. q.count('uncategorized_cashflow_transactions.id as total'); q.first(); @@ -83,14 +93,15 @@ export class GetBankAccountSummary { const totalUncategorizedTransactions = uncategorizedTranasctionsCount?.total || 0; const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0; - const totalExcludedTransactions = excludedTransactionsCount?.total || 0; + const totalPendingTransactions = pendingTransactionsCount?.total || 0; return { name: bankAccount.name, totalUncategorizedTransactions, totalRecognizedTransactions, totalExcludedTransactions, + totalPendingTransactions, }; } } diff --git a/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts b/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts index aed2fc945..4c1973f8b 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidSyncDB.ts @@ -25,6 +25,7 @@ import { Knex } from 'knex'; import uniqid from 'uniqid'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; +import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction'; const CONCURRENCY_ASYNC = 10; @@ -40,7 +41,7 @@ export class PlaidSyncDb { private cashflowApp: CashflowApplication; @Inject() - private deleteCashflowTransactionService: DeleteCashflowTransaction; + private removePendingTransaction: RemovePendingUncategorizedTransaction; @Inject() private eventPublisher: EventPublisher; @@ -185,21 +186,22 @@ export class PlaidSyncDb { plaidTransactionsIds: string[], trx?: Knex.Transaction ) { - const { CashflowTransaction } = this.tenancy.models(tenantId); + const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); - const cashflowTransactions = await CashflowTransaction.query(trx).whereIn( - 'plaidTransactionId', - plaidTransactionsIds - ); - const cashflowTransactionsIds = cashflowTransactions.map( + const uncategorizedTransactions = + await UncategorizedCashflowTransaction.query(trx).whereIn( + 'plaidTransactionId', + plaidTransactionsIds + ); + const uncategorizedTransactionsIds = uncategorizedTransactions.map( (trans) => trans.id ); await bluebird.map( - cashflowTransactionsIds, - (transactionId: number) => - this.deleteCashflowTransactionService.deleteCashflowTransaction( + uncategorizedTransactionsIds, + (uncategorizedTransactionId: number) => + this.removePendingTransaction.removePendingTransaction( tenantId, - transactionId, + uncategorizedTransactionId, trx ), { concurrency: CONCURRENCY_ASYNC } diff --git a/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts index 3265cc2ba..547aac45d 100644 --- a/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts +++ b/packages/server/src/services/Banking/Plaid/PlaidUpdateTransactions.ts @@ -73,6 +73,12 @@ export class PlaidUpdateTransactions { item, trx ); + // Sync removed transactions. + await this.plaidSync.syncRemoveTransactions( + tenantId, + removed?.map((r) => r.transaction_id), + trx + ); // Sync bank account transactions. await this.plaidSync.syncAccountsTransactions( tenantId, diff --git a/packages/server/src/services/Banking/Plaid/utils.ts b/packages/server/src/services/Banking/Plaid/utils.ts index 395b4346f..2804279c5 100644 --- a/packages/server/src/services/Banking/Plaid/utils.ts +++ b/packages/server/src/services/Banking/Plaid/utils.ts @@ -3,11 +3,11 @@ import { Item as PlaidItem, Institution as PlaidInstitution, AccountBase as PlaidAccount, + TransactionBase as PlaidTransactionBase, } from 'plaid'; import { CreateUncategorizedTransactionDTO, IAccountCreateDTO, - PlaidTransaction, } from '@/interfaces'; /** @@ -48,7 +48,7 @@ export const transformPlaidAccountToCreateAccount = R.curry( export const transformPlaidTrxsToCashflowCreate = R.curry( ( cashflowAccountId: number, - plaidTranasction: PlaidTransaction + plaidTranasction: PlaidTransactionBase ): CreateUncategorizedTransactionDTO => { return { date: plaidTranasction.date, @@ -64,6 +64,8 @@ export const transformPlaidTrxsToCashflowCreate = R.curry( accountId: cashflowAccountId, referenceNo: plaidTranasction.payment_meta?.reference_number, plaidTransactionId: plaidTranasction.transaction_id, + pending: plaidTranasction.pending, + pendingPlaidTransactionId: plaidTranasction.pending_transaction_id, }; } ); diff --git a/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts new file mode 100644 index 000000000..34bd5ad31 --- /dev/null +++ b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts @@ -0,0 +1,53 @@ +import { Inject } from 'typedi'; +import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { GetPendingBankAccountTransactionTransformer } from './GetPendingBankAccountTransactionTransformer'; + +export class GetPendingBankAccountTransactions { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private transformer: TransformerInjectable; + + /** + * Retrieves the given bank accounts pending transaction. + * @param {number} tenantId + * @param {number} bankAccountId + */ + async getPendingTransactions( + tenantId: number, + filter?: GetPendingTransactionsQuery + ) { + const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); + + const _filter = { + page: 1, + pageSize: 20, + ...filter, + }; + const { results, pagination } = + await UncategorizedCashflowTransaction.query() + .onBuild((q) => { + q.modify('pending'); + + if (_filter?.accountId) { + q.where('accountId', _filter.accountId); + } + }) + .pagination(_filter.page - 1, _filter.pageSize); + + const data = this.transformer.transform( + tenantId, + results, + new GetPendingBankAccountTransactionTransformer() + ); + return { data, pagination }; + } +} + +interface GetPendingTransactionsQuery { + page?: number; + pageSize?: number; + accountId?: number; +} diff --git a/packages/server/src/services/Cashflow/GetPendingBankAccountTransactionTransformer.ts b/packages/server/src/services/Cashflow/GetPendingBankAccountTransactionTransformer.ts new file mode 100644 index 000000000..d388546a4 --- /dev/null +++ b/packages/server/src/services/Cashflow/GetPendingBankAccountTransactionTransformer.ts @@ -0,0 +1,73 @@ +import { Transformer } from '@/lib/Transformer/Transformer'; +import { formatNumber } from '@/utils'; + +export class GetPendingBankAccountTransactionTransformer extends Transformer { + /** + * Include these attributes to sale invoice object. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return [ + 'formattedAmount', + 'formattedDate', + 'formattedDepositAmount', + 'formattedWithdrawalAmount', + ]; + }; + + /** + * Exclude all attributes. + * @returns {Array} + */ + public excludeAttributes = (): string[] => { + return []; + }; + + /** + * Formattes the transaction date. + * @param transaction + * @returns {string} + */ + public formattedDate(transaction) { + return this.formatDate(transaction.date); + } + + /** + * Formatted amount. + * @param transaction + * @returns {string} + */ + public formattedAmount(transaction) { + return formatNumber(transaction.amount, { + currencyCode: transaction.currencyCode, + }); + } + + /** + * Formatted deposit amount. + * @param transaction + * @returns {string} + */ + protected formattedDepositAmount(transaction) { + if (transaction.isDepositTransaction) { + return formatNumber(transaction.deposit, { + currencyCode: transaction.currencyCode, + }); + } + return ''; + } + + /** + * Formatted withdrawal amount. + * @param transaction + * @returns {string} + */ + protected formattedWithdrawalAmount(transaction) { + if (transaction.isWithdrawalTransaction) { + return formatNumber(transaction.withdrawal, { + currencyCode: transaction.currencyCode, + }); + } + return ''; + } +} diff --git a/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts b/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts new file mode 100644 index 000000000..15a75ae23 --- /dev/null +++ b/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts @@ -0,0 +1,58 @@ +import { Knex } from 'knex'; +import { Inject, Service } from 'typedi'; +import HasTenancyService from '../Tenancy/TenancyService'; +import UnitOfWork from '../UnitOfWork'; +import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; +import events from '@/subscribers/events'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; + +@Service() +export class RemovePendingUncategorizedTransaction { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + @Inject() + private eventPublisher: EventPublisher; + + /** + * REmoves the pending uncategorized transaction. + * @param {number} tenantId - + * @param {number} uncategorizedTransactionId - + * @param {Knex.Transaction} trx - + * @returns {Promise} + */ + public async removePendingTransaction( + tenantId: number, + uncategorizedTransactionId: number, + trx?: Knex.Transaction + ): Promise { + const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); + + const pendingTransaction = await UncategorizedCashflowTransaction.query(trx) + .findById(uncategorizedTransactionId) + .throwIfNotFound(); + + if (!pendingTransaction.isPending) { + throw new ServiceError(ERRORS.TRANSACTION_NOT_PENDING); + } + return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { + await this.eventPublisher.emitAsync( + events.bankTransactions.onPendingRemoving, + { tenantId, uncategorizedTransactionId, trx } + ); + // Removes the pending uncategorized transaction. + await UncategorizedCashflowTransaction.query(trx) + .findById(uncategorizedTransactionId) + .delete(); + + await this.eventPublisher.emitAsync( + events.bankTransactions.onPendingRemoved, + { tenantId, uncategorizedTransactionId, trx } + ); + }); + } +} diff --git a/packages/server/src/services/Cashflow/constants.ts b/packages/server/src/services/Cashflow/constants.ts index f9b8e24e8..50b35526a 100644 --- a/packages/server/src/services/Cashflow/constants.ts +++ b/packages/server/src/services/Cashflow/constants.ts @@ -15,10 +15,10 @@ export const ERRORS = { 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID', CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED', - - CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION', - TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED' - + CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: + 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION', + TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED', + TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING', }; export enum CASHFLOW_DIRECTION { diff --git a/packages/server/src/subscribers/events.ts b/packages/server/src/subscribers/events.ts index c72a2e7d3..2299df01b 100644 --- a/packages/server/src/subscribers/events.ts +++ b/packages/server/src/subscribers/events.ts @@ -659,6 +659,9 @@ export default { onUnexcluding: 'onBankTransactionUnexcluding', onUnexcluded: 'onBankTransactionUnexcluded', + + onPendingRemoving: 'onBankTransactionPendingRemoving', + onPendingRemoved: 'onBankTransactionPendingRemoved', }, bankAccount: { diff --git a/packages/webapp/src/constants/query-keys/banking.ts b/packages/webapp/src/constants/query-keys/banking.ts index 7164470e2..1a69341b0 100644 --- a/packages/webapp/src/constants/query-keys/banking.ts +++ b/packages/webapp/src/constants/query-keys/banking.ts @@ -7,4 +7,5 @@ export const BANK_QUERY_KEY = { 'RECOGNIZED_BANK_TRANSACTIONS_INFINITY', BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META', AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION', + PENDING_BANK_ACCOUNT_TRANSACTIONS: 'PENDING_BANK_ACCOUNT_TRANSACTIONS' }; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx index 3cd798feb..1c1320b54 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsFilterTabs.tsx @@ -20,7 +20,8 @@ export function AccountTransactionsFilterTabs() { const hasUncategorizedTransx = useMemo( () => bankAccountMetaSummary?.totalUncategorizedTransactions > 0 || - bankAccountMetaSummary?.totalExcludedTransactions > 0, + bankAccountMetaSummary?.totalExcludedTransactions > 0 || + bankAccountMetaSummary?.totalPendingTransactions > 0, [bankAccountMetaSummary], ); diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx index 2272c7e54..21e82f9d9 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AccountTransactionsUncategorizeFilter.tsx @@ -1,4 +1,6 @@ // @ts-nocheck +import * as R from 'ramda'; +import { useMemo } from 'react'; import { useAppQueryString } from '@/hooks'; import { Group } from '@/components'; import { useAccountTransactionsContext } from './AccountTransactionsProvider'; @@ -12,31 +14,49 @@ export function AccountTransactionsUncategorizeFilter() { bankAccountMetaSummary?.totalUncategorizedTransactions; const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions; + const totalPending = bankAccountMetaSummary?.totalPendingTransactions; + const handleTabsChange = (value) => { setLocationQuery({ uncategorizedFilter: value }); }; + const options = useMemo( + () => + R.when( + () => totalPending > 0, + R.append({ + value: 'pending', + label: ( + <> + Pending ({totalPending}) + + ), + }), + )([ + { + value: 'all', + label: ( + <> + All ({totalUncategorized}) + + ), + }, + { + value: 'recognized', + label: ( + <> + Recognized ({totalRecognized}) + + ), + }, + ]), + [totalPending, totalRecognized, totalUncategorized], + ); + return ( - All ({totalUncategorized}) - - ), - }, - { - value: 'recognized', - label: ( - <> - Recognized ({totalRecognized}) - - ), - }, - ]} + options={options} value={locationQuery?.uncategorizedFilter || 'all'} onValueChange={handleTabsChange} /> diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx index ad903b06d..54996f1a3 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx @@ -70,6 +70,8 @@ function AccountTransactionsSwitcher() { case 'all': default: return ; + case 'pending': + return null; } } diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webapp/src/hooks/query/bank-transaction.ts b/packages/webapp/src/hooks/query/bank-transaction.ts new file mode 100644 index 000000000..8cdfb88de --- /dev/null +++ b/packages/webapp/src/hooks/query/bank-transaction.ts @@ -0,0 +1,28 @@ +// @ts-nocheck +import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query'; +import useApiRequest from '../useRequest'; +import { BANK_QUERY_KEY } from '@/constants/query-keys/banking'; + +interface GetBankRuleRes {} + +/** + * Retrieve the given bank rule. + * @param {number} bankRuleId - + * @param {UseQueryOptions} options - + * @returns {UseQueryResult} + */ +export function usePendingBankAccountTransactions( + bankRuleId: number, + options?: UseQueryOptions, +): UseQueryResult { + const apiRequest = useApiRequest(); + + return useQuery( + [BANK_QUERY_KEY.PENDING_BANK_ACCOUNT_TRANSACTIONS], + () => + apiRequest + .get(`/banking/bank_account/pending_transactions`) + .then((res) => res.data), + { ...options }, + ); +} From 7054e862d5125bae6da3c24b63d1d2cf93a0c063 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 11 Aug 2024 22:51:58 +0200 Subject: [PATCH 2/4] feat: pending transactions table --- .../Banking/BankAccountsController.ts | 6 +- .../GetPendingBankAccountTransaction.ts | 2 +- .../src/constants/query-keys/banking.ts | 4 +- .../AllTransactionsUncategorized.tsx | 8 +- .../PendingTransactions.tsx | 14 +++ .../PendingTransactionsTable.tsx | 107 ++++++++++++++++++ .../PendingTransactionsTableBoot.tsx | 72 ++++++++++++ .../PendingTransactions/_hooks.tsx | 65 +++++++++++ packages/webapp/src/hooks/query/bank-rules.ts | 31 +++++ 9 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactions.tsx create mode 100644 packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/_hooks.tsx diff --git a/packages/server/src/api/controllers/Banking/BankAccountsController.ts b/packages/server/src/api/controllers/Banking/BankAccountsController.ts index 7f134fd75..e02aad096 100644 --- a/packages/server/src/api/controllers/Banking/BankAccountsController.ts +++ b/packages/server/src/api/controllers/Banking/BankAccountsController.ts @@ -29,7 +29,7 @@ export class BankAccountsController extends BaseController { [ query('account_id').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), ], this.validationResult, this.getBankAccountsPendingTransactions.bind(this) @@ -94,11 +94,13 @@ export class BankAccountsController extends BaseController { next: NextFunction ) { const { tenantId } = req; + const query = this.matchedQueryData(req); try { const data = await this.getPendingTransactionsService.getPendingTransactions( - tenantId + tenantId, + query ); return res.status(200).send(data); } catch (error) { diff --git a/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts index 34bd5ad31..11ad32576 100644 --- a/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts +++ b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts @@ -37,7 +37,7 @@ export class GetPendingBankAccountTransactions { }) .pagination(_filter.page - 1, _filter.pageSize); - const data = this.transformer.transform( + const data = await this.transformer.transform( tenantId, results, new GetPendingBankAccountTransactionTransformer() diff --git a/packages/webapp/src/constants/query-keys/banking.ts b/packages/webapp/src/constants/query-keys/banking.ts index 1a69341b0..26ec3135b 100644 --- a/packages/webapp/src/constants/query-keys/banking.ts +++ b/packages/webapp/src/constants/query-keys/banking.ts @@ -7,5 +7,7 @@ export const BANK_QUERY_KEY = { 'RECOGNIZED_BANK_TRANSACTIONS_INFINITY', BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META', AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION', - PENDING_BANK_ACCOUNT_TRANSACTIONS: 'PENDING_BANK_ACCOUNT_TRANSACTIONS' + PENDING_BANK_ACCOUNT_TRANSACTIONS: 'PENDING_BANK_ACCOUNT_TRANSACTIONS', + PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY: + 'PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY', }; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx index 54996f1a3..5e77c38f9 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/AllTransactionsUncategorized.tsx @@ -54,6 +54,12 @@ const AccountUncategorizedTransactions = lazy(() => ).then((module) => ({ default: module.AccountUncategorizedTransactionsAll })), ); +const PendingTransactions = lazy(() => + import('./PendingTransactions/PendingTransactions').then((module) => ({ + default: module.PendingTransactions, + })), +); + /** * Switches between the account transactions tables. * @returns {React.ReactNode} @@ -71,7 +77,7 @@ function AccountTransactionsSwitcher() { default: return ; case 'pending': - return null; + return ; } } diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactions.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactions.tsx new file mode 100644 index 000000000..34e813cc2 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactions.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { AccountTransactionsCard } from '../UncategorizedTransactions/AccountTransactionsCard'; +import { PendingTransactionsBoot } from './PendingTransactionsTableBoot'; +import { PendingTransactionsDataTable } from './PendingTransactionsTable'; + +export function PendingTransactions() { + return ( + + + + + + ); +} diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx index e69de29bb..ae3ecf944 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTable.tsx @@ -0,0 +1,107 @@ +// @ts-nocheck +import React from 'react'; +import clsx from 'classnames'; +import styled from 'styled-components'; +import { + DataTable, + TableFastCell, + TableSkeletonRows, + TableSkeletonHeader, + TableVirtualizedListRows, +} from '@/components'; +import withSettings from '@/containers/Settings/withSettings'; +import { withBankingActions } from '../../withBankingActions'; + +import { useAccountTransactionsContext } from '../AccountTransactionsProvider'; +import { usePendingTransactionsContext } from './PendingTransactionsTableBoot'; +import { usePendingTransactionsTableColumns } from './_hooks'; + +import { compose } from '@/utils'; + +/** + * Account transactions data table. + */ +function PendingTransactionsDataTableRoot({ + // #withSettings + cashflowTansactionsTableSize, +}) { + // Retrieve table columns. + const columns = usePendingTransactionsTableColumns(); + const { scrollableRef } = useAccountTransactionsContext(); + + // Retrieve list context. + const { pendingTransactions, isPendingTransactionsLoading } = + usePendingTransactionsContext(); + + return ( + + ); +} + +export const PendingTransactionsDataTable = compose( + withSettings(({ cashflowTransactionsSettings }) => ({ + cashflowTansactionsTableSize: cashflowTransactionsSettings?.tableSize, + })), + withBankingActions, +)(PendingTransactionsDataTableRoot); + +const DashboardConstrantTable = styled(DataTable)` + .table { + .thead { + .th { + background: #fff; + text-transform: uppercase; + letter-spacing: 1px; + font-size: 13px;i + font-weight: 500; + } + } + + .tbody { + .tr:last-child .td { + border-bottom: 0; + } + } + } +`; + +const CashflowTransactionsTable = styled(DashboardConstrantTable)` + .table .tbody { + .tbody-inner .tr.no-results { + .td { + padding: 2rem 0; + font-size: 14px; + color: #888; + font-weight: 400; + border-bottom: 0; + } + } + + .tbody-inner { + .tr .td:not(:first-child) { + border-left: 1px solid #e6e6e6; + } + + .td-description { + color: #5f6b7c; + } + } + } +`; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx index e69de29bb..9cd5eba44 100644 --- a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/PendingTransactionsTableBoot.tsx @@ -0,0 +1,72 @@ +// @ts-nocheck +import React from 'react'; +import { flatten, map } from 'lodash'; +import { IntersectionObserver } from '@/components'; +import { useAccountTransactionsContext } from '../AccountTransactionsProvider'; +import { usePendingBankTransactionsInfinity } from '@/hooks/query/bank-rules'; + +const PendingTransactionsContext = React.createContext(); + +function flattenInfinityPagesData(data) { + return flatten(map(data.pages, (page) => page.data)); +} + +/** + * Account pending transctions provider. + */ +function PendingTransactionsBoot({ children }) { + const { accountId } = useAccountTransactionsContext(); + + // Fetches the pending transactions. + const { + data: pendingTransactionsPage, + isFetching: isPendingTransactionFetching, + isLoading: isPendingTransactionsLoading, + isSuccess: isPendingTransactionsSuccess, + isFetchingNextPage: isPendingTransactionFetchNextPage, + fetchNextPage: fetchNextPendingTransactionsPage, + hasNextPage: hasPendingTransactionsNextPage, + } = usePendingBankTransactionsInfinity({ + account_id: accountId, + page_size: 50, + }); + // Memorized the cashflow account transactions. + const pendingTransactions = React.useMemo( + () => + isPendingTransactionsSuccess + ? flattenInfinityPagesData(pendingTransactionsPage) + : [], + [pendingTransactionsPage, isPendingTransactionsSuccess], + ); + // Handle the observer ineraction. + const handleObserverInteract = React.useCallback(() => { + if (!isPendingTransactionFetching && hasPendingTransactionsNextPage) { + fetchNextPendingTransactionsPage(); + } + }, [ + isPendingTransactionFetching, + hasPendingTransactionsNextPage, + fetchNextPendingTransactionsPage, + ]); + // Provider payload. + const provider = { + pendingTransactions, + isPendingTransactionFetching, + isPendingTransactionsLoading, + }; + + return ( + + {children} + + + ); +} + +const usePendingTransactionsContext = () => + React.useContext(PendingTransactionsContext); + +export { PendingTransactionsBoot, usePendingTransactionsContext }; diff --git a/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/_hooks.tsx b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/_hooks.tsx new file mode 100644 index 000000000..2ea2e20f3 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/AccountTransactions/PendingTransactions/_hooks.tsx @@ -0,0 +1,65 @@ +import { useMemo } from 'react'; +import intl from 'react-intl-universal'; + +/** + * Retrieve account pending transctions table columns. + */ +export function usePendingTransactionsTableColumns() { + return useMemo( + () => [ + { + id: 'date', + Header: intl.get('date'), + accessor: 'formatted_date', + width: 40, + clickable: true, + textOverview: true, + }, + { + id: 'description', + Header: 'Description', + accessor: 'description', + width: 160, + textOverview: true, + clickable: true, + }, + { + id: 'payee', + Header: 'Payee', + accessor: 'payee', + width: 60, + clickable: true, + textOverview: true, + }, + { + id: 'reference_number', + Header: 'Ref.#', + accessor: 'reference_no', + width: 50, + clickable: true, + textOverview: true, + }, + { + id: 'deposit', + Header: intl.get('cash_flow.label.deposit'), + accessor: 'formatted_deposit_amount', + width: 40, + className: 'deposit', + textOverview: true, + align: 'right', + clickable: true, + }, + { + id: 'withdrawal', + Header: intl.get('cash_flow.label.withdrawal'), + accessor: 'formatted_withdrawal_amount', + className: 'withdrawal', + width: 40, + textOverview: true, + align: 'right', + clickable: true, + }, + ], + [], + ); +} diff --git a/packages/webapp/src/hooks/query/bank-rules.ts b/packages/webapp/src/hooks/query/bank-rules.ts index 71697ec7e..1f72e7137 100644 --- a/packages/webapp/src/hooks/query/bank-rules.ts +++ b/packages/webapp/src/hooks/query/bank-rules.ts @@ -686,3 +686,34 @@ export function useExcludedBankTransactionsInfinity( }, ); } + +export function usePendingBankTransactionsInfinity( + query, + infinityProps, + axios, +) { + const apiRequest = useApiRequest(); + + return useInfiniteQuery( + [BANK_QUERY_KEY.PENDING_BANK_ACCOUNT_TRANSACTIONS_INFINITY, query], + async ({ pageParam = 1 }) => { + const response = await apiRequest.http({ + ...axios, + method: 'get', + url: `/api/banking/bank_accounts/pending_transactions`, + params: { page: pageParam, ...query }, + }); + return response.data; + }, + { + getPreviousPageParam: (firstPage) => firstPage.pagination.page - 1, + getNextPageParam: (lastPage) => { + const { pagination } = lastPage; + return pagination.total > pagination.page_size * pagination.page + ? lastPage.pagination.page + 1 + : undefined; + }, + ...infinityProps, + }, + ); +} From cb016be78c8d132a2d36df2f61eeacd284587914 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 12 Aug 2024 10:48:36 +0200 Subject: [PATCH 3/4] fix: avoid decrement/increment for pending bank transactions --- packages/server/src/interfaces/CashFlow.ts | 14 ++++++++++++++ .../BankAccounts/GetBankAccountSummary.ts | 9 +++++++++ .../Cashflow/GetRecongizedTransactions.ts | 2 ++ .../Cashflow/GetUncategorizedTransactions.ts | 2 ++ .../RemovePendingUncategorizedTransaction.ts | 18 ++++++++++++++++-- ...mentUncategorizedTransactionOnCategorize.ts | 17 ++++++++++++++--- 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/packages/server/src/interfaces/CashFlow.ts b/packages/server/src/interfaces/CashFlow.ts index 9e1d4bfd5..0b3298929 100644 --- a/packages/server/src/interfaces/CashFlow.ts +++ b/packages/server/src/interfaces/CashFlow.ts @@ -285,3 +285,17 @@ export interface IUncategorizedTransactionCreatedEventPayload { createUncategorizedTransactionDTO: CreateUncategorizedTransactionDTO; trx: Knex.Transaction; } + +export interface IPendingTransactionRemovingEventPayload { + tenantId: number; + uncategorizedTransactionId: number; + pendingTransaction: IUncategorizedCashflowTransaction; + trx?: Knex.Transaction; +} + +export interface IPendingTransactionRemovedEventPayload { + tenantId: number; + uncategorizedTransactionId: number; + pendingTransaction: IUncategorizedCashflowTransaction; + trx?: Knex.Transaction; +} diff --git a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts index 6c66c2a5e..4d7b7885a 100644 --- a/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts +++ b/packages/server/src/services/Banking/BankAccounts/GetBankAccountSummary.ts @@ -52,6 +52,9 @@ export class GetBankAccountSummary { q.withGraphJoined('matchedBankTransactions'); q.whereNull('matchedBankTransactions.id'); + // Exclude the pending transactions. + q.modify('notPending'); + // Count the results. q.count('uncategorized_cashflow_transactions.id as total'); q.first(); @@ -65,6 +68,9 @@ export class GetBankAccountSummary { q.withGraphJoined('recognizedTransaction'); q.whereNotNull('recognizedTransaction.id'); + // Exclude the pending transactions. + q.modify('notPending'); + // Count the results. q.count('uncategorized_cashflow_transactions.id as total'); q.first(); @@ -75,6 +81,9 @@ export class GetBankAccountSummary { q.where('accountId', bankAccountId); q.modify('excluded'); + // Exclude the pending transactions. + q.modify('notPending'); + // Count the results. q.count('uncategorized_cashflow_transactions.id as total'); q.first(); diff --git a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts index 9ba2bd102..e82d0b0c8 100644 --- a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts @@ -34,7 +34,9 @@ export class GetRecognizedTransactionsService { q.withGraphFetched('recognizedTransaction.assignAccount'); q.withGraphFetched('recognizedTransaction.bankRule'); q.whereNotNull('recognizedTransactionId'); + q.modify('notExcluded'); + q.modify('notPending'); if (_filter.accountId) { q.where('accountId', _filter.accountId); diff --git a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts index 7b3c76774..76625536c 100644 --- a/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetUncategorizedTransactions.ts @@ -51,7 +51,9 @@ export class GetUncategorizedTransactions { .onBuild((q) => { q.where('accountId', accountId); q.where('categorized', false); + q.modify('notExcluded'); + q.modify('notPending'); q.withGraphFetched('account'); q.withGraphFetched('recognizedTransaction.assignAccount'); diff --git a/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts b/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts index 15a75ae23..101a914cf 100644 --- a/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts +++ b/packages/server/src/services/Cashflow/RemovePendingUncategorizedTransaction.ts @@ -6,6 +6,10 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { ServiceError } from '@/exceptions'; import { ERRORS } from './constants'; +import { + IPendingTransactionRemovedEventPayload, + IPendingTransactionRemovingEventPayload, +} from '@/interfaces'; @Service() export class RemovePendingUncategorizedTransaction { @@ -42,7 +46,12 @@ export class RemovePendingUncategorizedTransaction { return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { await this.eventPublisher.emitAsync( events.bankTransactions.onPendingRemoving, - { tenantId, uncategorizedTransactionId, trx } + { + tenantId, + uncategorizedTransactionId, + pendingTransaction, + trx, + } as IPendingTransactionRemovingEventPayload ); // Removes the pending uncategorized transaction. await UncategorizedCashflowTransaction.query(trx) @@ -51,7 +60,12 @@ export class RemovePendingUncategorizedTransaction { await this.eventPublisher.emitAsync( events.bankTransactions.onPendingRemoved, - { tenantId, uncategorizedTransactionId, trx } + { + tenantId, + uncategorizedTransactionId, + pendingTransaction, + trx, + } as IPendingTransactionRemovedEventPayload ); }); } diff --git a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts index a1570a390..bd008967b 100644 --- a/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts +++ b/packages/server/src/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize.ts @@ -1,11 +1,11 @@ import { Inject, Service } from 'typedi'; +import PromisePool from '@supercharge/promise-pool'; import events from '@/subscribers/events'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { ICashflowTransactionCategorizedPayload, ICashflowTransactionUncategorizedPayload, } from '@/interfaces'; -import PromisePool from '@supercharge/promise-pool'; @Service() export class DecrementUncategorizedTransactionOnCategorize { @@ -36,13 +36,17 @@ export class DecrementUncategorizedTransactionOnCategorize { public async decrementUnCategorizedTransactionsOnCategorized({ tenantId, uncategorizedTransactions, - trx + trx, }: ICashflowTransactionCategorizedPayload) { const { Account } = this.tenancy.models(tenantId); await PromisePool.withConcurrency(1) .for(uncategorizedTransactions) .process(async (uncategorizedTransaction) => { + // Cannot continue if the transaction is still pending. + if (uncategorizedTransaction.isPending) { + return; + } await Account.query(trx) .findById(uncategorizedTransaction.accountId) .decrement('uncategorizedTransactions', 1); @@ -56,13 +60,17 @@ export class DecrementUncategorizedTransactionOnCategorize { public async incrementUnCategorizedTransactionsOnUncategorized({ tenantId, uncategorizedTransactions, - trx + trx, }: ICashflowTransactionUncategorizedPayload) { const { Account } = this.tenancy.models(tenantId); await PromisePool.withConcurrency(1) .for(uncategorizedTransactions) .process(async (uncategorizedTransaction) => { + // Cannot continue if the transaction is still pending. + if (uncategorizedTransaction.isPending) { + return; + } await Account.query(trx) .findById(uncategorizedTransaction.accountId) .increment('uncategorizedTransactions', 1); @@ -82,6 +90,9 @@ export class DecrementUncategorizedTransactionOnCategorize { if (!uncategorizedTransaction.accountId) return; + // Cannot continue if the transaction is still pending. + if (uncategorizedTransaction.isPending) return; + await Account.query(trx) .findById(uncategorizedTransaction.accountId) .increment('uncategorizedTransactions', 1); From be6f6e3c735a2b0d96425448620c3b723ae94d2e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Mon, 12 Aug 2024 10:54:16 +0200 Subject: [PATCH 4/4] fix: function description --- .../src/services/Cashflow/GetPendingBankAccountTransaction.ts | 4 ++-- .../server/src/services/Cashflow/GetRecongizedTransactions.ts | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts index 11ad32576..7f19af6a1 100644 --- a/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts +++ b/packages/server/src/services/Cashflow/GetPendingBankAccountTransaction.ts @@ -12,8 +12,8 @@ export class GetPendingBankAccountTransactions { /** * Retrieves the given bank accounts pending transaction. - * @param {number} tenantId - * @param {number} bankAccountId + * @param {number} tenantId - Tenant id. + * @param {GetPendingTransactionsQuery} filter - Pending transactions query. */ async getPendingTransactions( tenantId: number, diff --git a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts index e82d0b0c8..e0db018a4 100644 --- a/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts +++ b/packages/server/src/services/Cashflow/GetRecongizedTransactions.ts @@ -35,7 +35,10 @@ export class GetRecognizedTransactionsService { q.withGraphFetched('recognizedTransaction.bankRule'); q.whereNotNull('recognizedTransactionId'); + // Exclude the excluded transactions. q.modify('notExcluded'); + + // Exclude the pending transactions. q.modify('notPending'); if (_filter.accountId) {