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 (
+
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 (