From b71c79fef54e4d74f5eece4a2a7a6c6150eb10ae Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sun, 10 Mar 2024 02:53:57 +0200 Subject: [PATCH] feat: uncategorize the cashflow transaction --- .../Cashflow/DeleteCashflowTransaction.ts | 13 +++ ...transaction_id_to_cashflow_transactions.js | 15 ++++ .../server/src/interfaces/CashflowService.ts | 3 + packages/server/src/loaders/eventEmitter.ts | 2 + .../server/src/models/CashflowTransaction.ts | 9 ++ .../Cashflow/NewCashflowTransactionService.ts | 1 + .../server/src/services/Cashflow/constants.ts | 3 +- ...DeleteCashflowTransactionOnUncategorize.ts | 12 +++ .../PreventDeleteTransactionsOnDelete.ts | 37 +++++++++ .../server/src/services/Cashflow/utils.ts | 1 + .../containers/AlertsContainer/registered.tsx | 2 + .../src/containers/CashFlow/CashflowAlerts.ts | 16 ++++ .../UncategorizeTransactionAlert.tsx | 83 +++++++++++++++++++ .../UncategorizeTransactionAlert/index.ts | 1 + .../CashflowTransactionDrawerActionBar.tsx | 28 ++++++- .../src/hooks/query/cashflowAccounts.tsx | 23 +++++ 16 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js create mode 100644 packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts create mode 100644 packages/webapp/src/containers/CashFlow/CashflowAlerts.ts create mode 100644 packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/UncategorizeTransactionAlert.tsx create mode 100644 packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/index.ts diff --git a/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts b/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts index 1d0edece0..5e0a763d9 100644 --- a/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts +++ b/packages/server/src/api/controllers/Cashflow/DeleteCashflowTransaction.ts @@ -93,6 +93,19 @@ export default class DeleteCashflowTransactionController extends BaseController ], }); } + if ( + error.errorType === + 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED' + ) { + return res.boom.badRequest(null, { + errors: [ + { + type: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED', + code: 4100, + }, + ], + }); + } } next(error); } diff --git a/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js b/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js new file mode 100644 index 000000000..01b93bea5 --- /dev/null +++ b/packages/server/src/database/migrations/20240308122047_add_uncategorized_transaction_id_to_cashflow_transactions.js @@ -0,0 +1,15 @@ +exports.up = function (knex) { + return knex.schema.table('cashflow_transactions', (table) => { + table + .integer('uncategorized_transaction_id') + .unsigned() + .references('id') + .inTable('uncategorized_cashflow_transactions'); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('cashflow_transactions', (table) => { + table.dropColumn('uncategorized_transaction_id'); + }); +}; diff --git a/packages/server/src/interfaces/CashflowService.ts b/packages/server/src/interfaces/CashflowService.ts index acce307db..7d427b998 100644 --- a/packages/server/src/interfaces/CashflowService.ts +++ b/packages/server/src/interfaces/CashflowService.ts @@ -51,6 +51,7 @@ export interface ICashflowCommandDTO { export interface ICashflowNewCommandDTO extends ICashflowCommandDTO { plaidAccountId?: string; + uncategorizedTransactionId?: number; } export interface ICashflowTransaction { @@ -83,6 +84,8 @@ export interface ICashflowTransaction { isCashDebit?: boolean; isCashCredit?: boolean; + + uncategorizedTransactionId?: number; } export interface ICashflowTransactionLine { diff --git a/packages/server/src/loaders/eventEmitter.ts b/packages/server/src/loaders/eventEmitter.ts index ccc28df3d..584fb22d9 100644 --- a/packages/server/src/loaders/eventEmitter.ts +++ b/packages/server/src/loaders/eventEmitter.ts @@ -89,6 +89,7 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber'; import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent'; import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize'; +import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; } export default () => { return new EventPublisher(); @@ -217,5 +218,6 @@ export const susbcribers = () => { // Cashflow DeleteCashflowTransactionOnUncategorize, + PreventDeleteTransactionOnDelete ]; }; diff --git a/packages/server/src/models/CashflowTransaction.ts b/packages/server/src/models/CashflowTransaction.ts index 4e47d0e2d..3cc2baba7 100644 --- a/packages/server/src/models/CashflowTransaction.ts +++ b/packages/server/src/models/CashflowTransaction.ts @@ -13,6 +13,7 @@ export default class CashflowTransaction extends TenantModel { amount: number; exchangeRate: number; uncategorize: boolean; + uncategorizedTransaction!: boolean; /** * Table name. @@ -86,6 +87,14 @@ export default class CashflowTransaction extends TenantModel { return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN; } + /** + * Detarmines whether the transaction imported from uncategorized transaction. + * @returns {boolean} + */ + get isCategroizedTranasction() { + return !!this.uncategorizedTransaction; + } + /** * Relationship mapping. */ diff --git a/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts b/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts index 5222a2664..ecc0d3267 100644 --- a/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts +++ b/packages/server/src/services/Cashflow/NewCashflowTransactionService.ts @@ -86,6 +86,7 @@ export default class NewCashflowTransactionService { 'creditAccountId', 'branchId', 'plaidTransactionId', + 'uncategorizedTransactionId', ]); // Retreive the next invoice number. const autoNextNumber = diff --git a/packages/server/src/services/Cashflow/constants.ts b/packages/server/src/services/Cashflow/constants.ts index 293275855..bf448a549 100644 --- a/packages/server/src/services/Cashflow/constants.ts +++ b/packages/server/src/services/Cashflow/constants.ts @@ -11,7 +11,8 @@ export const ERRORS = { ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions', TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED', - UNCATEGORIZED_TRANSACTION_TYPE_INVALID: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID' + UNCATEGORIZED_TRANSACTION_TYPE_INVALID: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID', + CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED' }; export enum CASHFLOW_DIRECTION { diff --git a/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts index 3715b769c..75f595c65 100644 --- a/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts +++ b/packages/server/src/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize.ts @@ -2,12 +2,16 @@ import { Inject, Service } from 'typedi'; import events from '@/subscribers/events'; import { ICashflowTransactionUncategorizedPayload } from '@/interfaces'; import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService'; +import HasTenancyService from '@/services/Tenancy/TenancyService'; @Service() export class DeleteCashflowTransactionOnUncategorize { @Inject() private deleteCashflowTransactionService: DeleteCashflowTransaction; + @Inject() + private tenancy: HasTenancyService; + /** * Attaches events with handlers. */ @@ -27,10 +31,18 @@ export class DeleteCashflowTransactionOnUncategorize { oldUncategorizedTransaction, trx, }: ICashflowTransactionUncategorizedPayload) { + const { CashflowTransaction } = this.tenancy.models(tenantId); + // Deletes the cashflow transaction. if ( oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction' ) { + await CashflowTransaction.query() + .findById(oldUncategorizedTransaction.categorizeRefId) + .patch({ + uncategorizedTransactionId: null, + }); + await this.deleteCashflowTransactionService.deleteCashflowTransaction( tenantId, oldUncategorizedTransaction.categorizeRefId diff --git a/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts b/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts new file mode 100644 index 000000000..54cd25d72 --- /dev/null +++ b/packages/server/src/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete.ts @@ -0,0 +1,37 @@ +import { Service } from 'typedi'; +import events from '@/subscribers/events'; +import { ICommandCashflowDeletingPayload } from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from '../constants'; + +@Service() +export class PreventDeleteTransactionOnDelete { + /** + * Attaches events with handlers. + */ + public attach = (bus) => { + bus.subscribe( + events.cashflow.onTransactionDeleting, + this.preventDeleteCashflowTransactionHasUncategorizedTransaction.bind( + this + ) + ); + }; + + /** + * Prevent delete cashflow transaction has converted from uncategorized transaction. + * @param {ICommandCashflowDeletingPayload} payload + */ + public async preventDeleteCashflowTransactionHasUncategorizedTransaction({ + tenantId, + oldCashflowTransaction, + trx, + }: ICommandCashflowDeletingPayload) { + if (oldCashflowTransaction.uncategorizedTransactionId) { + throw new ServiceError( + ERRORS.CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED, + 'Cannot delete cashflow transaction converted from uncategorized transaction.' + ); + } + } +} diff --git a/packages/server/src/services/Cashflow/utils.ts b/packages/server/src/services/Cashflow/utils.ts index 7957b73a9..ce95e8416 100644 --- a/packages/server/src/services/Cashflow/utils.ts +++ b/packages/server/src/services/Cashflow/utils.ts @@ -61,6 +61,7 @@ export const transformCategorizeTransToCashflow = ( amount: uncategorizeModel.amount, transactionNumber: categorizeDTO.transactionNumber, transactionType: categorizeDTO.transactionType, + uncategorizedTransactionId: uncategorizeModel.id, publish: true, }; }; diff --git a/packages/webapp/src/containers/AlertsContainer/registered.tsx b/packages/webapp/src/containers/AlertsContainer/registered.tsx index d68666253..9bf51e222 100644 --- a/packages/webapp/src/containers/AlertsContainer/registered.tsx +++ b/packages/webapp/src/containers/AlertsContainer/registered.tsx @@ -25,6 +25,7 @@ import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/Warehouse import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts'; import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts'; import TaxRatesAlerts from '@/containers/TaxRates/alerts'; +import { CashflowAlerts } from '../CashFlow/CashflowAlerts'; export default [ ...AccountsAlerts, @@ -53,4 +54,5 @@ export default [ ...BranchesAlerts, ...ProjectAlerts, ...TaxRatesAlerts, + ...CashflowAlerts, ]; diff --git a/packages/webapp/src/containers/CashFlow/CashflowAlerts.ts b/packages/webapp/src/containers/CashFlow/CashflowAlerts.ts new file mode 100644 index 000000000..640de032d --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/CashflowAlerts.ts @@ -0,0 +1,16 @@ +// @ts-nocheck +import React from 'react'; + +const UncategorizeTransactionAlert = React.lazy( + () => import('./UncategorizeTransactionAlert/UncategorizeTransactionAlert'), +); + +/** + * Cashflow alerts. + */ +export const CashflowAlerts = [ + { + name: 'cashflow-tranaction-uncategorize', + component: UncategorizeTransactionAlert, + }, +]; diff --git a/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/UncategorizeTransactionAlert.tsx b/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/UncategorizeTransactionAlert.tsx new file mode 100644 index 000000000..0ef15fabe --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/UncategorizeTransactionAlert.tsx @@ -0,0 +1,83 @@ +// @ts-nocheck +import React from 'react'; +import { Intent, Alert } from '@blueprintjs/core'; +import { FormattedMessage as T } from '@/components'; +import { AppToaster } from '@/components'; + +import withAlertStoreConnect from '@/containers/Alert/withAlertStoreConnect'; +import withAlertActions from '@/containers/Alert/withAlertActions'; +import withDrawerActions from '@/containers/Drawer/withDrawerActions'; + +import { useUncategorizeTransaction } from '@/hooks/query'; +import { compose } from '@/utils'; +import { DRAWERS } from '@/constants/drawers'; + +/** + * Project delete alert. + */ +function UncategorizeTransactionAlert({ + name, + + // #withAlertStoreConnect + isOpen, + payload: { uncategorizedTransactionId }, + + // #withAlertActions + closeAlert, + + // #withDrawerActions + closeDrawer, +}) { + const { mutateAsync: uncategorizeTransaction, isLoading } = + useUncategorizeTransaction(); + + // handle cancel delete project alert. + const handleCancelDeleteAlert = () => { + closeAlert(name); + }; + + // handleConfirm delete project + const handleConfirmBtnClick = () => { + uncategorizeTransaction(uncategorizedTransactionId) + .then(() => { + AppToaster.show({ + message: 'The transaction has uncategorized successfully.', + intent: Intent.SUCCESS, + }); + closeAlert(name); + closeDrawer(DRAWERS.CASHFLOW_TRNASACTION_DETAILS); + }) + .catch( + ({ + response: { + data: { errors }, + }, + }) => { + AppToaster.show({ + message: 'Something went wrong.', + intent: Intent.DANGER, + }); + }, + ); + }; + + return ( + } + confirmButtonText={'Uncategorize'} + intent={Intent.WARNING} + isOpen={isOpen} + onCancel={handleCancelDeleteAlert} + onConfirm={handleConfirmBtnClick} + loading={isLoading} + > +

Are you sure want to uncategorize the transaction?

+
+ ); +} + +export default compose( + withAlertStoreConnect(), + withAlertActions, + withDrawerActions, +)(UncategorizeTransactionAlert); diff --git a/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/index.ts b/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/index.ts new file mode 100644 index 000000000..41a2b5dc3 --- /dev/null +++ b/packages/webapp/src/containers/CashFlow/UncategorizeTransactionAlert/index.ts @@ -0,0 +1 @@ +export * from './UncategorizeTransactionAlert'; \ No newline at end of file diff --git a/packages/webapp/src/containers/Drawers/CashflowTransactionDetailDrawer/CashflowTransactionDrawerActionBar.tsx b/packages/webapp/src/containers/Drawers/CashflowTransactionDetailDrawer/CashflowTransactionDrawerActionBar.tsx index ade249bae..2e0baa665 100644 --- a/packages/webapp/src/containers/Drawers/CashflowTransactionDetailDrawer/CashflowTransactionDrawerActionBar.tsx +++ b/packages/webapp/src/containers/Drawers/CashflowTransactionDetailDrawer/CashflowTransactionDrawerActionBar.tsx @@ -1,11 +1,18 @@ // @ts-nocheck import React from 'react'; -import { Button, Classes, NavbarGroup, Intent } from '@blueprintjs/core'; +import { + Button, + Classes, + NavbarGroup, + Intent, + NavbarDivider, +} from '@blueprintjs/core'; import { Can, FormattedMessage as T, DrawerActionsBar, Icon, + If, } from '@/components'; import withAlertsActions from '@/containers/Alert/withAlertActions'; import { useCashflowTransactionDrawerContext } from './CashflowTransactionDrawerProvider'; @@ -19,13 +26,22 @@ function CashflowTransactionDrawerActionBar({ // #withAlertsDialog openAlert, }) { - const { referenceId } = useCashflowTransactionDrawerContext(); + const { referenceId, cashflowTransaction } = + useCashflowTransactionDrawerContext(); // Handle cashflow transaction delete action. const handleDeleteCashflowTransaction = () => { openAlert('account-transaction-delete', { referenceId }); }; + // Handles the uncategorize button click. + const handleUncategorizeBtnClick = () => { + openAlert('cashflow-tranaction-uncategorize', { + uncategorizedTransactionId: + cashflowTransaction.uncategorized_transaction_id, + }); + }; + return ( @@ -37,6 +53,14 @@ function CashflowTransactionDrawerActionBar({ intent={Intent.DANGER} onClick={handleDeleteCashflowTransaction} /> + + +