From b37002bea6efedc2df50207390fabdfda4add39a Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 20 Jun 2024 13:50:29 +0200 Subject: [PATCH] feat: exclude/unexclude the uncategorized transactions --- .../ExcludeBankTransactionsController.ts | 90 +++++++++++++++++++ .../Cashflow/CashflowController.ts | 2 + ...categorized_cashflow_transactions_table.js | 11 +++ .../UncategorizedCashflowTransaction.ts | 1 + .../Banking/Exclude/ExcludeBankTransaction.ts | 41 +++++++++ .../ExcludeBankTransactionsApplication.ts | 38 ++++++++ .../Exclude/UnexcludeBankTransaction.ts | 41 +++++++++ .../src/services/Banking/Exclude/utils.ts | 14 +++ 8 files changed, 238 insertions(+) create mode 100644 packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts create mode 100644 packages/server/src/database/migrations/20240620111308_add_excluded_column_to_uncategorized_cashflow_transactions_table.js create mode 100644 packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts create mode 100644 packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts create mode 100644 packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts create mode 100644 packages/server/src/services/Banking/Exclude/utils.ts diff --git a/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts b/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts new file mode 100644 index 000000000..6cf3a92d3 --- /dev/null +++ b/packages/server/src/api/controllers/Banking/ExcludeBankTransactionsController.ts @@ -0,0 +1,90 @@ +import { Inject, Service } from 'typedi'; +import { param } from 'express-validator'; +import { NextFunction, Request, Response, Router } from 'express'; +import BaseController from '../BaseController'; +import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication'; + +@Service() +export class ExcludeBankTransactionsController extends BaseController { + @Inject() + prviate excludeBankTransactionApp: ExcludeBankTransactionsApplication; + + /** + * Router constructor. + */ + public router() { + const router = Router(); + + router.put( + '/transactions/:transactionId/exclude', + [param('transactionId').exists()], + this.validationResult, + this.excludeBankTransaction.bind(this) + ); + router.put( + '/transactions/:transactionId/unexclude', + [param('transactionId').exists()], + this.validationResult, + this.unexcludeBankTransaction.bind(this) + ); + return router; + } + + /** + * Marks a bank transaction as excluded. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns + */ + private async excludeBankTransaction( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const { tenantId } = req; + const { transactionId } = req.params; + + try { + await this.excludeBankTransactionApp.excludeBankTransaction( + tenantId, + transactionId + ); + return res.status(200).send({ + message: 'The bank transaction has been excluded.', + id: transactionId, + }); + } catch (error) { + next(error); + } + } + + /** + * Marks a bank transaction as not excluded. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + * @returns {Promise} + */ + private async unexcludeBankTransaction( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const { tenantId } = req; + const { transactionId } = req.params; + + try { + await this.excludeBankTransactionApp.unexcludeBankTransaction( + tenantId, + transactionId + ); + return res.status(200).send({ + message: 'The bank transaction has been unexcluded.', + id: transactionId, + }); + } catch (error) { + next(error); + } + } +} diff --git a/packages/server/src/api/controllers/Cashflow/CashflowController.ts b/packages/server/src/api/controllers/Cashflow/CashflowController.ts index 42efa4c48..4ef98d267 100644 --- a/packages/server/src/api/controllers/Cashflow/CashflowController.ts +++ b/packages/server/src/api/controllers/Cashflow/CashflowController.ts @@ -4,6 +4,7 @@ import CommandCashflowTransaction from './NewCashflowTransaction'; import DeleteCashflowTransaction from './DeleteCashflowTransaction'; import GetCashflowTransaction from './GetCashflowTransaction'; import GetCashflowAccounts from './GetCashflowAccounts'; +import { ExcludeBankTransactionsController } from '../Banking/ExcludeBankTransactionsController'; @Service() export default class CashflowController { @@ -14,6 +15,7 @@ export default class CashflowController { const router = Router(); router.use(Container.get(CommandCashflowTransaction).router()); + router.use(Container.get(ExcludeBankTransactionsController).router()); router.use(Container.get(GetCashflowTransaction).router()); router.use(Container.get(GetCashflowAccounts).router()); router.use(Container.get(DeleteCashflowTransaction).router()); diff --git a/packages/server/src/database/migrations/20240620111308_add_excluded_column_to_uncategorized_cashflow_transactions_table.js b/packages/server/src/database/migrations/20240620111308_add_excluded_column_to_uncategorized_cashflow_transactions_table.js new file mode 100644 index 000000000..735b817ed --- /dev/null +++ b/packages/server/src/database/migrations/20240620111308_add_excluded_column_to_uncategorized_cashflow_transactions_table.js @@ -0,0 +1,11 @@ +exports.up = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.boolean('excluded'); + }); +}; + +exports.down = function (knex) { + return knex.schema.table('uncategorized_cashflow_transactions', (table) => { + table.dropColumn('excluded'); + }); +}; diff --git a/packages/server/src/models/UncategorizedCashflowTransaction.ts b/packages/server/src/models/UncategorizedCashflowTransaction.ts index 272458892..373da0a27 100644 --- a/packages/server/src/models/UncategorizedCashflowTransaction.ts +++ b/packages/server/src/models/UncategorizedCashflowTransaction.ts @@ -44,6 +44,7 @@ export default class UncategorizedCashflowTransaction extends mixin( 'deposit', 'isDepositTransaction', 'isWithdrawalTransaction', + 'isRecognized', ]; } diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts new file mode 100644 index 000000000..7d23a0f22 --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransaction.ts @@ -0,0 +1,41 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { Inject, Service } from 'typedi'; +import { validateTransactionNotCategorized } from './utils'; + +@Service() +export class ExcludeBankTransaction { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Marks the given bank transaction as excluded. + * @param {number} tenantId + * @param {number} bankTransactionId + * @returns {Promise} + */ + public async excludeBankTransaction( + tenantId: number, + bankTransactionId: number + ) { + const { UncategorizeCashflowTransaction } = this.tenancy.models(tenantId); + + const oldUncategorizedTransaction = + await UncategorizeCashflowTransaction.query() + .findById(bankTransactionId) + .throwIfNotFound(); + + validateTransactionNotCategorized(oldUncategorizedTransaction); + + return this.uow.withTransaction(tenantId, async (trx) => { + await UncategorizeCashflowTransaction.query(trx) + .findById(bankTransactionId) + .patch({ + excluded: true, + }); + }); + } +} diff --git a/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts new file mode 100644 index 000000000..3ee664a64 --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/ExcludeBankTransactionsApplication.ts @@ -0,0 +1,38 @@ +import { Inject, Service } from 'typedi'; +import { ExcludeBankTransaction } from './ExcludeBankTransaction'; +import { UnexcludeBankTransaction } from './UnexcludeBankTransaction'; + +@Service() +export class ExcludeBankTransactionsApplication { + @Inject() + private excludeBankTransactionService: ExcludeBankTransaction; + + @Inject() + private unexcludeBankTransactionService: UnexcludeBankTransaction; + + /** + * Marks a bank transaction as excluded. + * @param {number} tenantId - The ID of the tenant. + * @param {number} bankTransactionId - The ID of the bank transaction to exclude. + * @returns {Promise} + */ + public excludeBankTransaction(tenantId: number, bankTransactionId: number) { + return this.excludeBankTransactionService.excludeBankTransaction( + tenantId, + bankTransactionId + ); + } + + /** + * Marks a bank transaction as not excluded. + * @param {number} tenantId - The ID of the tenant. + * @param {number} bankTransactionId - The ID of the bank transaction to exclude. + * @returns {Promise} + */ + public unexcludeBankTransaction(tenantId: number, bankTransactionId: number) { + return this.unexcludeBankTransactionService.unexcludeBankTransaction( + tenantId, + bankTransactionId + ); + } +} diff --git a/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts new file mode 100644 index 000000000..159fe373a --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/UnexcludeBankTransaction.ts @@ -0,0 +1,41 @@ +import HasTenancyService from '@/services/Tenancy/TenancyService'; +import UnitOfWork from '@/services/UnitOfWork'; +import { Inject, Service } from 'typedi'; +import { validateTransactionNotCategorized } from './utils'; + +@Service() +export class UnexcludeBankTransaction { + @Inject() + private tenancy: HasTenancyService; + + @Inject() + private uow: UnitOfWork; + + /** + * Marks the given bank transaction as excluded. + * @param {number} tenantId + * @param {number} bankTransactionId + * @returns {Promise} + */ + public async unexcludeBankTransaction( + tenantId: number, + bankTransactionId: number + ) { + const { UncategorizeCashflowTransaction } = this.tenancy.models(tenantId); + + const oldUncategorizedTransaction = + await UncategorizeCashflowTransaction.query() + .findById(bankTransactionId) + .throwIfNotFound(); + + validateTransactionNotCategorized(oldUncategorizedTransaction); + + return this.uow.withTransaction(tenantId, async (trx) => { + await UncategorizeCashflowTransaction.query(trx) + .findById(bankTransactionId) + .patch({ + excluded: false, + }); + }); + } +} diff --git a/packages/server/src/services/Banking/Exclude/utils.ts b/packages/server/src/services/Banking/Exclude/utils.ts new file mode 100644 index 000000000..6d4f02a9a --- /dev/null +++ b/packages/server/src/services/Banking/Exclude/utils.ts @@ -0,0 +1,14 @@ +import { ServiceError } from '@/exceptions'; +import UncategorizedCashflowTransaction from '@/models/UncategorizedCashflowTransaction'; + +const ERRORS = { + TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED', +}; + +export const validateTransactionNotCategorized = ( + transaction: UncategorizedCashflowTransaction +) => { + if (transaction.categorized) { + throw new ServiceError(ERRORS.TRANSACTION_ALREADY_CATEGORIZED); + } +};