feat: uncategorize the cashflow transaction

This commit is contained in:
Ahmed Bouhuolia
2024-03-10 02:53:57 +02:00
parent 2baf407814
commit b71c79fef5
16 changed files with 246 additions and 3 deletions

View File

@@ -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); next(error);
} }

View File

@@ -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');
});
};

View File

@@ -51,6 +51,7 @@ export interface ICashflowCommandDTO {
export interface ICashflowNewCommandDTO extends ICashflowCommandDTO { export interface ICashflowNewCommandDTO extends ICashflowCommandDTO {
plaidAccountId?: string; plaidAccountId?: string;
uncategorizedTransactionId?: number;
} }
export interface ICashflowTransaction { export interface ICashflowTransaction {
@@ -83,6 +84,8 @@ export interface ICashflowTransaction {
isCashDebit?: boolean; isCashDebit?: boolean;
isCashCredit?: boolean; isCashCredit?: boolean;
uncategorizedTransactionId?: number;
} }
export interface ICashflowTransactionLine { export interface ICashflowTransactionLine {

View File

@@ -89,6 +89,7 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber'; import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent'; import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize'; import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -217,5 +218,6 @@ export const susbcribers = () => {
// Cashflow // Cashflow
DeleteCashflowTransactionOnUncategorize, DeleteCashflowTransactionOnUncategorize,
PreventDeleteTransactionOnDelete
]; ];
}; };

View File

@@ -13,6 +13,7 @@ export default class CashflowTransaction extends TenantModel {
amount: number; amount: number;
exchangeRate: number; exchangeRate: number;
uncategorize: boolean; uncategorize: boolean;
uncategorizedTransaction!: boolean;
/** /**
* Table name. * Table name.
@@ -86,6 +87,14 @@ export default class CashflowTransaction extends TenantModel {
return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN; return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN;
} }
/**
* Detarmines whether the transaction imported from uncategorized transaction.
* @returns {boolean}
*/
get isCategroizedTranasction() {
return !!this.uncategorizedTransaction;
}
/** /**
* Relationship mapping. * Relationship mapping.
*/ */

View File

@@ -86,6 +86,7 @@ export default class NewCashflowTransactionService {
'creditAccountId', 'creditAccountId',
'branchId', 'branchId',
'plaidTransactionId', 'plaidTransactionId',
'uncategorizedTransactionId',
]); ]);
// Retreive the next invoice number. // Retreive the next invoice number.
const autoNextNumber = const autoNextNumber =

View File

@@ -11,7 +11,8 @@ export const ERRORS = {
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions', ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED', 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 { export enum CASHFLOW_DIRECTION {

View File

@@ -2,12 +2,16 @@ import { Inject, Service } from 'typedi';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces'; import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService'; import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class DeleteCashflowTransactionOnUncategorize { export class DeleteCashflowTransactionOnUncategorize {
@Inject() @Inject()
private deleteCashflowTransactionService: DeleteCashflowTransaction; private deleteCashflowTransactionService: DeleteCashflowTransaction;
@Inject()
private tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.
*/ */
@@ -27,10 +31,18 @@ export class DeleteCashflowTransactionOnUncategorize {
oldUncategorizedTransaction, oldUncategorizedTransaction,
trx, trx,
}: ICashflowTransactionUncategorizedPayload) { }: ICashflowTransactionUncategorizedPayload) {
const { CashflowTransaction } = this.tenancy.models(tenantId);
// Deletes the cashflow transaction. // Deletes the cashflow transaction.
if ( if (
oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction' oldUncategorizedTransaction.categorizeRefType === 'CashflowTransaction'
) { ) {
await CashflowTransaction.query()
.findById(oldUncategorizedTransaction.categorizeRefId)
.patch({
uncategorizedTransactionId: null,
});
await this.deleteCashflowTransactionService.deleteCashflowTransaction( await this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId, tenantId,
oldUncategorizedTransaction.categorizeRefId oldUncategorizedTransaction.categorizeRefId

View File

@@ -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.'
);
}
}
}

View File

@@ -61,6 +61,7 @@ export const transformCategorizeTransToCashflow = (
amount: uncategorizeModel.amount, amount: uncategorizeModel.amount,
transactionNumber: categorizeDTO.transactionNumber, transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType, transactionType: categorizeDTO.transactionType,
uncategorizedTransactionId: uncategorizeModel.id,
publish: true, publish: true,
}; };
}; };

View File

@@ -25,6 +25,7 @@ import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/Warehouse
import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts'; import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts';
import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts'; import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
import TaxRatesAlerts from '@/containers/TaxRates/alerts'; import TaxRatesAlerts from '@/containers/TaxRates/alerts';
import { CashflowAlerts } from '../CashFlow/CashflowAlerts';
export default [ export default [
...AccountsAlerts, ...AccountsAlerts,
@@ -53,4 +54,5 @@ export default [
...BranchesAlerts, ...BranchesAlerts,
...ProjectAlerts, ...ProjectAlerts,
...TaxRatesAlerts, ...TaxRatesAlerts,
...CashflowAlerts,
]; ];

View File

@@ -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,
},
];

View File

@@ -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 (
<Alert
cancelButtonText={<T id={'cancel'} />}
confirmButtonText={'Uncategorize'}
intent={Intent.WARNING}
isOpen={isOpen}
onCancel={handleCancelDeleteAlert}
onConfirm={handleConfirmBtnClick}
loading={isLoading}
>
<p>Are you sure want to uncategorize the transaction?</p>
</Alert>
);
}
export default compose(
withAlertStoreConnect(),
withAlertActions,
withDrawerActions,
)(UncategorizeTransactionAlert);

View File

@@ -0,0 +1 @@
export * from './UncategorizeTransactionAlert';

View File

@@ -1,11 +1,18 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { Button, Classes, NavbarGroup, Intent } from '@blueprintjs/core'; import {
Button,
Classes,
NavbarGroup,
Intent,
NavbarDivider,
} from '@blueprintjs/core';
import { import {
Can, Can,
FormattedMessage as T, FormattedMessage as T,
DrawerActionsBar, DrawerActionsBar,
Icon, Icon,
If,
} from '@/components'; } from '@/components';
import withAlertsActions from '@/containers/Alert/withAlertActions'; import withAlertsActions from '@/containers/Alert/withAlertActions';
import { useCashflowTransactionDrawerContext } from './CashflowTransactionDrawerProvider'; import { useCashflowTransactionDrawerContext } from './CashflowTransactionDrawerProvider';
@@ -19,13 +26,22 @@ function CashflowTransactionDrawerActionBar({
// #withAlertsDialog // #withAlertsDialog
openAlert, openAlert,
}) { }) {
const { referenceId } = useCashflowTransactionDrawerContext(); const { referenceId, cashflowTransaction } =
useCashflowTransactionDrawerContext();
// Handle cashflow transaction delete action. // Handle cashflow transaction delete action.
const handleDeleteCashflowTransaction = () => { const handleDeleteCashflowTransaction = () => {
openAlert('account-transaction-delete', { referenceId }); openAlert('account-transaction-delete', { referenceId });
}; };
// Handles the uncategorize button click.
const handleUncategorizeBtnClick = () => {
openAlert('cashflow-tranaction-uncategorize', {
uncategorizedTransactionId:
cashflowTransaction.uncategorized_transaction_id,
});
};
return ( return (
<Can I={CashflowAction.Delete} a={AbilitySubject.Cashflow}> <Can I={CashflowAction.Delete} a={AbilitySubject.Cashflow}>
<DrawerActionsBar> <DrawerActionsBar>
@@ -37,6 +53,14 @@ function CashflowTransactionDrawerActionBar({
intent={Intent.DANGER} intent={Intent.DANGER}
onClick={handleDeleteCashflowTransaction} onClick={handleDeleteCashflowTransaction}
/> />
<If condition={cashflowTransaction.uncategorized_transaction_id}>
<NavbarDivider />
<Button
text={'Uncategorize'}
onClick={handleUncategorizeBtnClick}
className={Classes.MINIMAL}
/>
</If>
</NavbarGroup> </NavbarGroup>
</DrawerActionsBar> </DrawerActionsBar>
</Can> </Can>

View File

@@ -256,3 +256,26 @@ export function useCategorizeTransaction(props) {
}, },
); );
} }
/**
* Uncategorize the cashflow transaction.
*/
export function useUncategorizeTransaction(props) {
const queryClient = useQueryClient();
const apiRequest = useApiRequest();
return useMutation(
(id: number) => apiRequest.post(`cashflow/transactions/${id}/uncategorize`),
{
onSuccess: (res, id) => {
// Invalidate queries.
commonInvalidateQueries(queryClient);
queryClient.invalidateQueries(t.CASHFLOW_UNCAATEGORIZED_TRANSACTION);
queryClient.invalidateQueries(
t.CASHFLOW_ACCOUNT_UNCATEGORIZED_TRANSACTIONS_INFINITY,
);
},
...props,
},
);
}