mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 08:10:32 +00:00
feat: Pending bank transactions
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { NextFunction, Request, Response, Router } from 'express';
|
import { NextFunction, Request, Response, Router } from 'express';
|
||||||
|
import { param, query } from 'express-validator';
|
||||||
import BaseController from '@/api/controllers/BaseController';
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
|
||||||
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
|
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
|
||||||
import { param } from 'express-validator';
|
import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BankAccountsController extends BaseController {
|
export class BankAccountsController extends BaseController {
|
||||||
@@ -13,6 +14,9 @@ export class BankAccountsController extends BaseController {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private bankAccountsApp: BankAccountsApplication;
|
private bankAccountsApp: BankAccountsApplication;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getPendingTransactionsService: GetPendingBankAccountTransactions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Router constructor.
|
* Router constructor.
|
||||||
*/
|
*/
|
||||||
@@ -20,6 +24,16 @@ export class BankAccountsController extends BaseController {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
|
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(
|
router.post(
|
||||||
'/:bankAccountId/disconnect',
|
'/:bankAccountId/disconnect',
|
||||||
this.disconnectBankAccount.bind(this)
|
this.disconnectBankAccount.bind(this)
|
||||||
@@ -27,17 +41,13 @@ export class BankAccountsController extends BaseController {
|
|||||||
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
|
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
|
||||||
router.post(
|
router.post(
|
||||||
'/:bankAccountId/pause_feeds',
|
'/:bankAccountId/pause_feeds',
|
||||||
[
|
[param('bankAccountId').exists().isNumeric().toInt()],
|
||||||
param('bankAccountId').exists().isNumeric().toInt(),
|
|
||||||
],
|
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.pauseBankAccountFeeds.bind(this)
|
this.pauseBankAccountFeeds.bind(this)
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
'/:bankAccountId/resume_feeds',
|
'/:bankAccountId/resume_feeds',
|
||||||
[
|
[param('bankAccountId').exists().isNumeric().toInt()],
|
||||||
param('bankAccountId').exists().isNumeric().toInt(),
|
|
||||||
],
|
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.resumeBankAccountFeeds.bind(this)
|
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.
|
* Disonnect the given bank account.
|
||||||
* @param {Request} req
|
* @param {Request} req
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -268,6 +268,8 @@ export interface CreateUncategorizedTransactionDTO {
|
|||||||
description?: string;
|
description?: string;
|
||||||
referenceNo?: string | null;
|
referenceNo?: string | null;
|
||||||
plaidTransactionId?: string | null;
|
plaidTransactionId?: string | null;
|
||||||
|
pending?: boolean;
|
||||||
|
pendingPlaidTransactionId?: string | null;
|
||||||
batch?: string;
|
batch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
plaidTransactionId!: string;
|
plaidTransactionId!: string;
|
||||||
recognizedTransactionId!: number;
|
recognizedTransactionId!: number;
|
||||||
excludedAt: Date;
|
excludedAt: Date;
|
||||||
|
pending: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table name.
|
* Table name.
|
||||||
@@ -46,7 +47,8 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
'isDepositTransaction',
|
'isDepositTransaction',
|
||||||
'isWithdrawalTransaction',
|
'isWithdrawalTransaction',
|
||||||
'isRecognized',
|
'isRecognized',
|
||||||
'isExcluded'
|
'isExcluded',
|
||||||
|
'isPending',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +101,14 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
return !!this.excludedAt;
|
return !!this.excludedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the transaction is pending.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public get isPending(): boolean {
|
||||||
|
return !!this.pending;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model modifiers.
|
* Model modifiers.
|
||||||
*/
|
*/
|
||||||
@@ -143,6 +153,20 @@ export default class UncategorizedCashflowTransaction extends mixin(
|
|||||||
query.whereNull('categorizeRefType');
|
query.whereNull('categorizeRefType');
|
||||||
query.whereNull('categorizeRefId');
|
query.whereNull('categorizeRefId');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the not pending transactions.
|
||||||
|
*/
|
||||||
|
notPending(query) {
|
||||||
|
query.where('pending', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the pending transactions.
|
||||||
|
*/
|
||||||
|
pending(query) {
|
||||||
|
query.where('pending', true);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,22 @@ export class GetBankAccountSummary {
|
|||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
q.count('uncategorized_cashflow_transactions.id as total');
|
||||||
q.first();
|
q.first();
|
||||||
});
|
});
|
||||||
|
// Retrieves excluded transactions count.
|
||||||
const excludedTransactionsCount =
|
const excludedTransactionsCount =
|
||||||
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
await UncategorizedCashflowTransaction.query().onBuild((q) => {
|
||||||
q.where('accountId', bankAccountId);
|
q.where('accountId', bankAccountId);
|
||||||
q.modify('excluded');
|
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.
|
// Count the results.
|
||||||
q.count('uncategorized_cashflow_transactions.id as total');
|
q.count('uncategorized_cashflow_transactions.id as total');
|
||||||
q.first();
|
q.first();
|
||||||
@@ -83,14 +93,15 @@ export class GetBankAccountSummary {
|
|||||||
const totalUncategorizedTransactions =
|
const totalUncategorizedTransactions =
|
||||||
uncategorizedTranasctionsCount?.total || 0;
|
uncategorizedTranasctionsCount?.total || 0;
|
||||||
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
const totalRecognizedTransactions = recognizedTransactionsCount?.total || 0;
|
||||||
|
|
||||||
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
|
const totalExcludedTransactions = excludedTransactionsCount?.total || 0;
|
||||||
|
const totalPendingTransactions = pendingTransactionsCount?.total || 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: bankAccount.name,
|
name: bankAccount.name,
|
||||||
totalUncategorizedTransactions,
|
totalUncategorizedTransactions,
|
||||||
totalRecognizedTransactions,
|
totalRecognizedTransactions,
|
||||||
totalExcludedTransactions,
|
totalExcludedTransactions,
|
||||||
|
totalPendingTransactions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { Knex } from 'knex';
|
|||||||
import uniqid from 'uniqid';
|
import uniqid from 'uniqid';
|
||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
|
import { RemovePendingUncategorizedTransaction } from '@/services/Cashflow/RemovePendingUncategorizedTransaction';
|
||||||
|
|
||||||
const CONCURRENCY_ASYNC = 10;
|
const CONCURRENCY_ASYNC = 10;
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ export class PlaidSyncDb {
|
|||||||
private cashflowApp: CashflowApplication;
|
private cashflowApp: CashflowApplication;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private deleteCashflowTransactionService: DeleteCashflowTransaction;
|
private removePendingTransaction: RemovePendingUncategorizedTransaction;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
@@ -185,21 +186,22 @@ export class PlaidSyncDb {
|
|||||||
plaidTransactionsIds: string[],
|
plaidTransactionsIds: string[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
const { CashflowTransaction } = this.tenancy.models(tenantId);
|
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
const cashflowTransactions = await CashflowTransaction.query(trx).whereIn(
|
const uncategorizedTransactions =
|
||||||
'plaidTransactionId',
|
await UncategorizedCashflowTransaction.query(trx).whereIn(
|
||||||
plaidTransactionsIds
|
'plaidTransactionId',
|
||||||
);
|
plaidTransactionsIds
|
||||||
const cashflowTransactionsIds = cashflowTransactions.map(
|
);
|
||||||
|
const uncategorizedTransactionsIds = uncategorizedTransactions.map(
|
||||||
(trans) => trans.id
|
(trans) => trans.id
|
||||||
);
|
);
|
||||||
await bluebird.map(
|
await bluebird.map(
|
||||||
cashflowTransactionsIds,
|
uncategorizedTransactionsIds,
|
||||||
(transactionId: number) =>
|
(uncategorizedTransactionId: number) =>
|
||||||
this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
this.removePendingTransaction.removePendingTransaction(
|
||||||
tenantId,
|
tenantId,
|
||||||
transactionId,
|
uncategorizedTransactionId,
|
||||||
trx
|
trx
|
||||||
),
|
),
|
||||||
{ concurrency: CONCURRENCY_ASYNC }
|
{ concurrency: CONCURRENCY_ASYNC }
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ export class PlaidUpdateTransactions {
|
|||||||
item,
|
item,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
// Sync removed transactions.
|
||||||
|
await this.plaidSync.syncRemoveTransactions(
|
||||||
|
tenantId,
|
||||||
|
removed?.map((r) => r.transaction_id),
|
||||||
|
trx
|
||||||
|
);
|
||||||
// Sync bank account transactions.
|
// Sync bank account transactions.
|
||||||
await this.plaidSync.syncAccountsTransactions(
|
await this.plaidSync.syncAccountsTransactions(
|
||||||
tenantId,
|
tenantId,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import {
|
|||||||
Item as PlaidItem,
|
Item as PlaidItem,
|
||||||
Institution as PlaidInstitution,
|
Institution as PlaidInstitution,
|
||||||
AccountBase as PlaidAccount,
|
AccountBase as PlaidAccount,
|
||||||
|
TransactionBase as PlaidTransactionBase,
|
||||||
} from 'plaid';
|
} from 'plaid';
|
||||||
import {
|
import {
|
||||||
CreateUncategorizedTransactionDTO,
|
CreateUncategorizedTransactionDTO,
|
||||||
IAccountCreateDTO,
|
IAccountCreateDTO,
|
||||||
PlaidTransaction,
|
|
||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +48,7 @@ export const transformPlaidAccountToCreateAccount = R.curry(
|
|||||||
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
export const transformPlaidTrxsToCashflowCreate = R.curry(
|
||||||
(
|
(
|
||||||
cashflowAccountId: number,
|
cashflowAccountId: number,
|
||||||
plaidTranasction: PlaidTransaction
|
plaidTranasction: PlaidTransactionBase
|
||||||
): CreateUncategorizedTransactionDTO => {
|
): CreateUncategorizedTransactionDTO => {
|
||||||
return {
|
return {
|
||||||
date: plaidTranasction.date,
|
date: plaidTranasction.date,
|
||||||
@@ -64,6 +64,8 @@ export const transformPlaidTrxsToCashflowCreate = R.curry(
|
|||||||
accountId: cashflowAccountId,
|
accountId: cashflowAccountId,
|
||||||
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
referenceNo: plaidTranasction.payment_meta?.reference_number,
|
||||||
plaidTransactionId: plaidTranasction.transaction_id,
|
plaidTransactionId: plaidTranasction.transaction_id,
|
||||||
|
pending: plaidTranasction.pending,
|
||||||
|
pendingPlaidTransactionId: plaidTranasction.pending_transaction_id,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<string>}
|
||||||
|
*/
|
||||||
|
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 '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
public async removePendingTransaction(
|
||||||
|
tenantId: number,
|
||||||
|
uncategorizedTransactionId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<void> {
|
||||||
|
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 }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,10 +15,10 @@ export const ERRORS = {
|
|||||||
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
|
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
|
||||||
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
|
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
|
||||||
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
||||||
|
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION:
|
||||||
CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION: 'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
|
'CANNOT_CATEGORIZE_EXCLUDED_TRANSACTION',
|
||||||
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED'
|
TRANSACTION_NOT_CATEGORIZED: 'TRANSACTION_NOT_CATEGORIZED',
|
||||||
|
TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum CASHFLOW_DIRECTION {
|
export enum CASHFLOW_DIRECTION {
|
||||||
|
|||||||
@@ -659,6 +659,9 @@ export default {
|
|||||||
|
|
||||||
onUnexcluding: 'onBankTransactionUnexcluding',
|
onUnexcluding: 'onBankTransactionUnexcluding',
|
||||||
onUnexcluded: 'onBankTransactionUnexcluded',
|
onUnexcluded: 'onBankTransactionUnexcluded',
|
||||||
|
|
||||||
|
onPendingRemoving: 'onBankTransactionPendingRemoving',
|
||||||
|
onPendingRemoved: 'onBankTransactionPendingRemoved',
|
||||||
},
|
},
|
||||||
|
|
||||||
bankAccount: {
|
bankAccount: {
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export const BANK_QUERY_KEY = {
|
|||||||
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
'RECOGNIZED_BANK_TRANSACTIONS_INFINITY',
|
||||||
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
BANK_ACCOUNT_SUMMARY_META: 'BANK_ACCOUNT_SUMMARY_META',
|
||||||
AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
|
AUTOFILL_CATEGORIZE_BANK_TRANSACTION: 'AUTOFILL_CATEGORIZE_BANK_TRANSACTION',
|
||||||
|
PENDING_BANK_ACCOUNT_TRANSACTIONS: 'PENDING_BANK_ACCOUNT_TRANSACTIONS'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export function AccountTransactionsFilterTabs() {
|
|||||||
const hasUncategorizedTransx = useMemo(
|
const hasUncategorizedTransx = useMemo(
|
||||||
() =>
|
() =>
|
||||||
bankAccountMetaSummary?.totalUncategorizedTransactions > 0 ||
|
bankAccountMetaSummary?.totalUncategorizedTransactions > 0 ||
|
||||||
bankAccountMetaSummary?.totalExcludedTransactions > 0,
|
bankAccountMetaSummary?.totalExcludedTransactions > 0 ||
|
||||||
|
bankAccountMetaSummary?.totalPendingTransactions > 0,
|
||||||
[bankAccountMetaSummary],
|
[bankAccountMetaSummary],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
import * as R from 'ramda';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useAppQueryString } from '@/hooks';
|
import { useAppQueryString } from '@/hooks';
|
||||||
import { Group } from '@/components';
|
import { Group } from '@/components';
|
||||||
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
import { useAccountTransactionsContext } from './AccountTransactionsProvider';
|
||||||
@@ -12,31 +14,49 @@ export function AccountTransactionsUncategorizeFilter() {
|
|||||||
bankAccountMetaSummary?.totalUncategorizedTransactions;
|
bankAccountMetaSummary?.totalUncategorizedTransactions;
|
||||||
const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions;
|
const totalRecognized = bankAccountMetaSummary?.totalRecognizedTransactions;
|
||||||
|
|
||||||
|
const totalPending = bankAccountMetaSummary?.totalPendingTransactions;
|
||||||
|
|
||||||
const handleTabsChange = (value) => {
|
const handleTabsChange = (value) => {
|
||||||
setLocationQuery({ uncategorizedFilter: value });
|
setLocationQuery({ uncategorizedFilter: value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
R.when(
|
||||||
|
() => totalPending > 0,
|
||||||
|
R.append({
|
||||||
|
value: 'pending',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Pending <strong>({totalPending})</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)([
|
||||||
|
{
|
||||||
|
value: 'all',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
All <strong>({totalUncategorized})</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'recognized',
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Recognized <strong>({totalRecognized})</strong>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
[totalPending, totalRecognized, totalUncategorized],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group position={'apart'}>
|
<Group position={'apart'}>
|
||||||
<TagsControl
|
<TagsControl
|
||||||
options={[
|
options={options}
|
||||||
{
|
|
||||||
value: 'all',
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
All <strong>({totalUncategorized})</strong>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'recognized',
|
|
||||||
label: (
|
|
||||||
<>
|
|
||||||
Recognized <strong>({totalRecognized})</strong>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
value={locationQuery?.uncategorizedFilter || 'all'}
|
value={locationQuery?.uncategorizedFilter || 'all'}
|
||||||
onValueChange={handleTabsChange}
|
onValueChange={handleTabsChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ function AccountTransactionsSwitcher() {
|
|||||||
case 'all':
|
case 'all':
|
||||||
default:
|
default:
|
||||||
return <AccountUncategorizedTransactions />;
|
return <AccountUncategorizedTransactions />;
|
||||||
|
case 'pending':
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
packages/webapp/src/hooks/query/bank-transaction.ts
Normal file
28
packages/webapp/src/hooks/query/bank-transaction.ts
Normal file
@@ -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<GetBankRuleRes, Error>} options -
|
||||||
|
* @returns {UseQueryResult<GetBankRuleRes, Error>}
|
||||||
|
*/
|
||||||
|
export function usePendingBankAccountTransactions(
|
||||||
|
bankRuleId: number,
|
||||||
|
options?: UseQueryOptions<GetBankRuleRes, Error>,
|
||||||
|
): UseQueryResult<GetBankRuleRes, Error> {
|
||||||
|
const apiRequest = useApiRequest();
|
||||||
|
|
||||||
|
return useQuery<GetBankRuleRes, Error>(
|
||||||
|
[BANK_QUERY_KEY.PENDING_BANK_ACCOUNT_TRANSACTIONS],
|
||||||
|
() =>
|
||||||
|
apiRequest
|
||||||
|
.get(`/banking/bank_account/pending_transactions`)
|
||||||
|
.then((res) => res.data),
|
||||||
|
{ ...options },
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user