diff --git a/client/src/services/axios.js b/client/src/services/axios.js index f69dda9bd..e24f81c53 100644 --- a/client/src/services/axios.js +++ b/client/src/services/axios.js @@ -9,7 +9,6 @@ import { setGlobalErrors } from 'store/globalErrors/globalErrors.actions'; const http = axios.create(); - http.interceptors.request.use((request) => { const state = store.getState(); const { token, organization } = state.authentication; diff --git a/server/src/http/controllers/AccountTypes.js b/server/src/http/controllers/AccountTypes.js deleted file mode 100644 index a2e187dcb..000000000 --- a/server/src/http/controllers/AccountTypes.js +++ /dev/null @@ -1,32 +0,0 @@ -import express from 'express'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/', - this.getAccountTypesList.validation, - asyncMiddleware(this.getAccountTypesList.handler)); - - return router; - }, - - /** - * Retrieve accounts types list. - */ - getAccountTypesList: { - validation: [], - async handler(req, res) { - const { AccountType } = req.models; - const accountTypes = await AccountType.query(); - - return res.status(200).send({ - account_types: accountTypes, - }); - }, - }, -}; diff --git a/server/src/http/controllers/AccountTypes.ts b/server/src/http/controllers/AccountTypes.ts new file mode 100644 index 000000000..a2886b7aa --- /dev/null +++ b/server/src/http/controllers/AccountTypes.ts @@ -0,0 +1,31 @@ +import { Service } from 'typedi'; +import { Request, Response, Router } from 'express'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import BaseController from '@/http/controllers/BaseController'; + +@Service() +export default class AccountsTypesController extends BaseController{ + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get('/', + asyncMiddleware(this.getAccountTypesList)); + + return router; + } + + /** + * Retrieve accounts types list. + */ + async getAccountTypesList(req: Request, res: Response) { + const { AccountType } = req.models; + const accountTypes = await AccountType.query(); + + return res.status(200).send({ + account_types: accountTypes, + }); + } +}; diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js deleted file mode 100644 index b6b69abaf..000000000 --- a/server/src/http/controllers/Accounts.js +++ /dev/null @@ -1,667 +0,0 @@ -import express from 'express'; -import { check, validationResult, param, query } from 'express-validator'; -import { difference } from 'lodash'; -import asyncMiddleware from '@/http/middleware/asyncMiddleware'; -import JournalPoster from '@/services/Accounting/JournalPoster'; -import { - mapViewRolesToConditionals, - mapFilterRolesToDynamicFilter, -} from '@/lib/ViewRolesBuilder'; -import { - DynamicFilter, - DynamicFilterSortBy, - DynamicFilterViews, - DynamicFilterFilterRoles, -} from '@/lib/DynamicFilter'; - -export default { - /** - * Router constructor method. - */ - router() { - const router = express.Router(); - - router.post( - '/', - this.newAccount.validation, - asyncMiddleware(this.newAccount.handler) - ); - - router.post( - '/:id', - this.editAccount.validation, - asyncMiddleware(this.editAccount.handler) - ); - - router.get( - '/:id', - this.getAccount.validation, - asyncMiddleware(this.getAccount.handler) - ); - - router.get( - '/', - this.getAccountsList.validation, - asyncMiddleware(this.getAccountsList.handler) - ); - - router.delete( - '/', - this.deleteBulkAccounts.validation, - asyncMiddleware(this.deleteBulkAccounts.handler) - ); - - router.delete( - '/:id', - this.deleteAccount.validation, - asyncMiddleware(this.deleteAccount.handler) - ); - - router.post( - '/:id/active', - this.activeAccount.validation, - asyncMiddleware(this.activeAccount.handler) - ); - - router.post( - '/:id/inactive', - this.inactiveAccount.validation, - asyncMiddleware(this.inactiveAccount.handler) - ); - - router.post( - '/:id/recalculate-balance', - this.recalcualteBalanace.validation, - asyncMiddleware(this.recalcualteBalanace.handler) - ); - - router.post( - '/:id/transfer_account/:toAccount', - this.transferToAnotherAccount.validation, - asyncMiddleware(this.transferToAnotherAccount.handler) - ); - - router.post( - '/bulk/:type(activate|inactivate)', - this.bulkInactivateAccounts.validation, - asyncMiddleware(this.bulkInactivateAccounts.handler) - ); - - return router; - }, - - /** - * Creates a new account. - */ - newAccount: { - validation: [ - check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(), - check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(), - check('account_type_id').exists().isNumeric().toInt(), - check('description').optional().isLength({ max: 512 }).trim().escape(), - check('parent_account_id') - .optional({ nullable: true }) - .isNumeric() - .toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const form = { ...req.body }; - const { AccountType, Account } = req.models; - - const foundAccountCodePromise = form.code - ? Account.query().where('code', form.code) - : null; - - const foundAccountTypePromise = AccountType.query().findById( - form.account_type_id - ); - const [foundAccountCode, foundAccountType] = await Promise.all([ - foundAccountCodePromise, - foundAccountTypePromise, - ]); - if (foundAccountCodePromise && foundAccountCode.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'NOT_UNIQUE_CODE', code: 100 }], - }); - } - if (!foundAccountType) { - return res.boom.badRequest(null, { - errors: [{ type: 'NOT_EXIST_ACCOUNT_TYPE', code: 200 }], - }); - } - if (form.parent_account_id) { - const parentAccount = await Account.query() - .where('id', form.parent_account_id) - .first(); - - if (!parentAccount) { - return res.boom.badRequest(null, { - errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 300 }], - }); - } - if (parentAccount.accountTypeId !== form.parent_account_id) { - return res.boom.badRequest(null, { - errors: [ - { type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 }, - ], - }); - } - } - const insertedAccount = await Account.query().insertAndFetch({ ...form }); - - return res.status(200).send({ account: { ...insertedAccount } }); - }, - }, - - /** - * Edit the given account details. - */ - editAccount: { - validation: [ - param('id').exists().toInt(), - check('name').exists().isLength({ min: 3, max: 255 }).trim().escape(), - check('code').optional().isLength({ min: 3, max: 6 }).trim().escape(), - check('account_type_id').exists().isNumeric().toInt(), - check('description').optional().isLength({ max: 512 }).trim().escape(), - ], - async handler(req, res) { - const { id } = req.params; - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const { Account, AccountType } = req.models; - const form = { ...req.body }; - const account = await Account.query().findById(id); - - if (!account) { - return res.boom.notFound(); - } - const errorReasons = []; - - // Validate the account type is not changed. - if (account.account_type_id != form.accountTypeId) { - errorReasons.push({ - type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', - code: 100, - }); - } - // Validate the account code not exists on the storage. - if (form.code && form.code !== account.code) { - const foundAccountCode = await Account.query() - .where('code', form.code) - .whereNot('id', account.id); - - if (foundAccountCode.length > 0) { - errorReasons.push({ type: 'NOT_UNIQUE_CODE', code: 200 }); - } - } - if (form.parent_account_id) { - const parentAccount = await Account.query() - .where('id', form.parent_account_id) - .whereNot('id', account.id) - .first(); - - if (!parentAccount) { - errorReasons.push({ - type: 'PARENT_ACCOUNT_NOT_FOUND', - code: 300, - }); - } - if (parentAccount.accountTypeId !== account.parentAccountId) { - return res.boom.badRequest(null, { - errors: [ - { type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 400 }, - ], - }); - } - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - // Update the account on the storage. - const updatedAccount = await Account.query().patchAndFetchById( - account.id, - { ...form } - ); - - return res.status(200).send({ account: { ...updatedAccount } }); - }, - }, - - /** - * Get details of the given account. - */ - getAccount: { - validation: [param('id').toInt()], - async handler(req, res) { - const { id } = req.params; - const { Account } = req.models; - const account = await Account.query().where('id', id).first(); - - if (!account) { - return res.boom.notFound(); - } - return res.status(200).send({ account }); - }, - }, - - /** - * Delete the given account. - */ - deleteAccount: { - validation: [param('id').toInt()], - async handler(req, res) { - const { id } = req.params; - const { Account, AccountTransaction } = req.models; - const account = await Account.query().findById(id); - - if (!account) { - return res.boom.notFound(); - } - if (account.predefined) { - return res.boom.badRequest(null, { - errors: [{ type: 'ACCOUNT.PREDEFINED', code: 200 }], - }); - } - - // Validate the account has no child accounts. - const childAccounts = await Account.query().where( - 'parent_account_id', - account.id - ); - - if (childAccounts.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 300 }], - }); - } - const accountTransactions = await AccountTransaction.query().where( - 'account_id', - account.id - ); - if (accountTransactions.length > 0) { - return res.boom.badRequest(null, { - errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 100 }], - }); - } - await Account.query().deleteById(account.id); - - return res.status(200).send(); - }, - }, - - /** - * Retrieve accounts list. - */ - getAccountsList: { - validation: [ - query('display_type').optional().isIn(['tree', 'flat']), - query('account_types').optional().isArray(), - query('account_types.*').optional().isNumeric().toInt(), - query('custom_view_id').optional().isNumeric().toInt(), - - query('stringified_filter_roles').optional().isJSON(), - - query('column_sort_by').optional(), - query('sort_order').optional().isIn(['desc', 'asc']), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const filter = { - display_type: 'flat', - account_types: [], - filter_roles: [], - sort_order: 'asc', - ...req.query, - }; - if (filter.stringified_filter_roles) { - filter.filter_roles = JSON.parse(filter.stringified_filter_roles); - } - const { Resource, Account, View } = req.models; - const errorReasons = []; - - const accountsResource = await Resource.query() - .remember() - .where('name', 'accounts') - .withGraphFetched('fields') - .first(); - - if (!accountsResource) { - return res.status(400).send({ - errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }], - }); - } - const resourceFieldsKeys = accountsResource.fields.map((c) => c.key); - - const view = await View.query().onBuild((builder) => { - if (filter.custom_view_id) { - builder.where('id', filter.custom_view_id); - } else { - builder.where('favourite', true); - } - // builder.where('resource_id', accountsResource.id); - builder.withGraphFetched('roles.field'); - builder.withGraphFetched('columns'); - builder.first(); - - builder.remember(); - }); - const dynamicFilter = new DynamicFilter(Account.tableName); - - if (filter.column_sort_by) { - if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) { - errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); - } - const sortByFilter = new DynamicFilterSortBy( - filter.column_sort_by, - filter.sort_order - ); - dynamicFilter.setFilter(sortByFilter); - } - // View roles. - if (view && view.roles.length > 0) { - const viewFilter = new DynamicFilterViews( - mapViewRolesToConditionals(view.roles), - view.rolesLogicExpression - ); - if (!viewFilter.validateFilterRoles()) { - errorReasons.push({ - type: 'VIEW.LOGIC.EXPRESSION.INVALID', - code: 400, - }); - } - dynamicFilter.setFilter(viewFilter); - } - // Filter roles. - if (filter.filter_roles.length > 0) { - // Validate the accounts resource fields. - const filterRoles = new DynamicFilterFilterRoles( - mapFilterRolesToDynamicFilter(filter.filter_roles), - accountsResource.fields - ); - dynamicFilter.setFilter(filterRoles); - - if (filterRoles.validateFilterRoles().length > 0) { - errorReasons.push({ - type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', - code: 500, - }); - } - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - - const accounts = await Account.query().onBuild((builder) => { - builder.modify('filterAccountTypes', filter.account_types); - builder.withGraphFetched('type'); - builder.withGraphFetched('balance'); - - dynamicFilter.buildQuery()(builder); - }); - return res.status(200).send({ - accounts: - filter.display_type === 'tree' - ? Account.toNestedArray(accounts) - : accounts, - ...(view - ? { - customViewId: view.id, - } - : {}), - }); - }, - }, - - /** - * Re-calculates balance of the given account. - */ - recalcualteBalanace: { - validation: [param('id').isNumeric().toInt()], - async handler(req, res) { - const { id } = req.params; - const { Account, AccountTransaction, AccountBalance } = req.models; - const account = await Account.findById(id); - - if (!account) { - return res.status(400).send({ - errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], - }); - } - const accountTransactions = AccountTransaction.query().where( - 'account_id', - account.id - ); - - const journalEntries = new JournalPoster(); - journalEntries.loadFromCollection(accountTransactions); - - // Delete the balance of the given account id. - await AccountBalance.query().where('account_id', account.id).delete(); - - // Save calcualted account balance. - await journalEntries.saveBalance(); - - return res.status(200).send(); - }, - }, - - /** - * Active the given account. - */ - activeAccount: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id } = req.params; - const { Account } = req.models; - const account = await Account.query().findById(id); - - if (!account) { - return res.status(400).send({ - errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], - }); - } - await Account.query().where('id', id).patch({ active: true }); - - return res.status(200).send({ id: account.id }); - }, - }, - - /** - * Inactive the given account. - */ - inactiveAccount: { - validation: [param('id').exists().isNumeric().toInt()], - async handler(req, res) { - const { id } = req.params; - const { Account } = req.models; - const account = await Account.query().findById(id); - - if (!account) { - return res.status(400).send({ - errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], - }); - } - await Account.query().where('id', id).patch({ active: false }); - - return res.status(200).send({ id: account.id }); - }, - }, - - /** - * Transfer all journal entries of the given account to another account. - */ - transferToAnotherAccount: { - validation: [ - param('id').exists().isNumeric().toInt(), - param('toAccount').exists().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - - // const { id, toAccount: toAccountId } = req.params; - - // const [fromAccount, toAccount] = await Promise.all([ - // Account.query().findById(id), - // Account.query().findById(toAccountId), - // ]); - - // const fromAccountTransactions = await AccountTransaction.query() - // .where('account_id', fromAccount); - - // return res.status(200).send(); - }, - }, - - deleteBulkAccounts: { - validation: [ - query('ids').isArray({ min: 1 }), - query('ids.*').isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const filter = { ids: [], ...req.query }; - const { Account, AccountTransaction } = req.models; - - const accounts = await Account.query().onBuild((builder) => { - if (filter.ids.length) { - builder.whereIn('id', filter.ids); - } - }); - const accountsIds = accounts.map((a) => a.id); - const notFoundAccounts = difference(filter.ids, accountsIds); - const predefinedAccounts = accounts.filter( - (account) => account.predefined - ); - const errorReasons = []; - - if (notFoundAccounts.length > 0) { - return res.status(404).send({ - errors: [ - { - type: 'ACCOUNTS.IDS.NOT.FOUND', - code: 200, - ids: notFoundAccounts, - }, - ], - }); - } - if (predefinedAccounts.length > 0) { - errorReasons.push({ - type: 'ACCOUNT.PREDEFINED', - code: 200, - ids: predefinedAccounts.map((a) => a.id), - }); - } - const accountsTransactions = await AccountTransaction.query() - .whereIn('account_id', accountsIds) - .count('id as transactions_count') - .groupBy('account_id') - .select('account_id'); - - const accountsHasTransactions = []; - - accountsTransactions.forEach((transaction) => { - if (transaction.transactionsCount > 0) { - accountsHasTransactions.push(transaction.accountId); - } - }); - if (accountsHasTransactions.length > 0) { - errorReasons.push({ - type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', - code: 300, - ids: accountsHasTransactions, - }); - } - if (errorReasons.length > 0) { - return res.status(400).send({ errors: errorReasons }); - } - await Account.query() - .whereIn( - 'id', - accounts.map((a) => a.id) - ) - .delete(); - - return res.status(200).send(); - }, - }, - - /** - * Bulk acvtivate/inactivate the given accounts. - */ - bulkInactivateAccounts: { - validation: [ - query('ids').isArray({ min: 1 }), - query('ids.*').isNumeric().toInt(), - param('type').exists().isIn(['activate', 'inactivate']), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', - ...validationErrors, - }); - } - const filter = { - ids: [], - ...req.query, - }; - const { Account } = req.models; - const { type } = req.params; - - const storedAccounts = await Account.query().whereIn('id', filter.ids); - const storedAccountsIds = storedAccounts.map((account) => account.id); - const notFoundAccounts = difference(filter.ids, storedAccountsIds); - - if (notFoundAccounts.length > 0) { - return res.status(400).send({ - errors: [{ type: 'ACCOUNTS.NOT.FOUND', code: 200 }], - }); - } - const updatedAccounts = await Account.query() - .whereIn('id', storedAccountsIds) - .patch({ - active: type === 'activate' ? 1 : 0, - }); - - return res.status(200).send({ ids: storedAccountsIds }); - }, - }, -}; diff --git a/server/src/http/controllers/Accounts.ts b/server/src/http/controllers/Accounts.ts new file mode 100644 index 000000000..31d7212a8 --- /dev/null +++ b/server/src/http/controllers/Accounts.ts @@ -0,0 +1,617 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { check, validationResult, param, query } from 'express-validator'; +import { difference } from 'lodash'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import JournalPoster from '@/services/Accounting/JournalPoster'; +import { + mapViewRolesToConditionals, + mapFilterRolesToDynamicFilter, +} from '@/lib/ViewRolesBuilder'; +import { + DynamicFilter, + DynamicFilterSortBy, + DynamicFilterViews, + DynamicFilterFilterRoles, +} from '@/lib/DynamicFilter'; +import BaseController from './BaseController'; +import { IAccountDTO, IAccount } from '@/interfaces'; +import { ServiceError } from '@/exceptions'; +import AccountsService from '@/services/Accounts/AccountsService'; +import { Service, Inject } from 'typedi'; + +@Service() +export default class AccountsController extends BaseController{ + + @Inject() + accountsService: AccountsService; + + /** + * Router constructor method. + */ + router() { + const router = Router(); + + router.post( + '/bulk/:type(activate|inactivate)', + asyncMiddleware(this.bulkToggleActivateAccounts.bind(this)) + ); + router.post( + '/:id/activate', [ + ...this.accountParamSchema, + ], + asyncMiddleware(this.activateAccount.bind(this)) + ); + router.post( + '/:id/inactivate', [ + ...this.accountParamSchema, + ], + asyncMiddleware(this.inactivateAccount.bind(this)) + ); + router.post( + '/:id', [ + ...this.accountDTOSchema, + ...this.accountParamSchema, + ], + this.validationResult, + asyncMiddleware(this.editAccount.bind(this)) + ); + router.post( + '/', [ + ...this.accountDTOSchema, + ], + this.validationResult, + asyncMiddleware(this.newAccount.bind(this)) + ); + router.get( + '/:id', [ + ...this.accountParamSchema, + ], + this.validationResult, + asyncMiddleware(this.getAccount.bind(this)) + ); + // // router.get( + // // '/', [ + // // ...this.accountsListSchema + // // ], + // // asyncMiddleware(this.getAccountsList.handler) + // // ); + + router.delete( + '/:id', [ + ...this.accountParamSchema + ], + this.validationResult, + asyncMiddleware(this.deleteAccount.bind(this)) + ); + router.delete( + '/', + this.bulkDeleteSchema, + asyncMiddleware(this.deleteBulkAccounts.bind(this)) + ); + + // router.post( + // '/:id/recalculate-balance', + // this.recalcualteBalanace.validation, + // asyncMiddleware(this.recalcualteBalanace.handler) + // ); + // router.post( + // '/:id/transfer_account/:toAccount', + // this.transferToAnotherAccount.validation, + // asyncMiddleware(this.transferToAnotherAccount.handler) + // ); + + + return router; + } + + /** + * Account DTO Schema validation. + */ + get accountDTOSchema() { + return [ + check('name') + .exists() + .isLength({ min: 3, max: 255 }) + .trim() + .escape(), + check('code') + .optional({ nullable: true }) + .isLength({ min: 3, max: 6 }) + .trim() + .escape(), + check('account_type_id') + .exists() + .isNumeric() + .toInt(), + check('description') + .optional({ nullable: true }) + .isLength({ max: 512 }) + .trim() + .escape(), + check('parent_account_id') + .optional({ nullable: true }) + .isNumeric() + .toInt(), + ]; + } + + /** + * Account param schema validation. + */ + get accountParamSchema() { + return [ + param('id').exists().isNumeric().toInt() + ]; + } + + /** + * Accounts list schema validation. + */ + get accountsListSchema() { + return [ + query('display_type').optional().isIn(['tree', 'flat']), + query('account_types').optional().isArray(), + query('account_types.*').optional().isNumeric().toInt(), + query('custom_view_id').optional().isNumeric().toInt(), + + query('stringified_filter_roles').optional().isJSON(), + + query('column_sort_by').optional(), + query('sort_order').optional().isIn(['desc', 'asc']), + ]; + } + + get bulkDeleteSchema() { + return [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ]; + } + + /** + * Creates a new account. + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - + */ + async newAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const accountDTO: IAccountDTO = this.matchedBodyData(req); + + try { + const account = await this.accountsService.newAccount(tenantId, accountDTO); + + return res.status(200).send({ id: account.id }); + } catch (error) { + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Edit account details. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + async editAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: accountId } = req.params; + const accountDTO: IAccountDTO = this.matchedBodyData(req); + + try { + const account = await this.accountsService.editAccount(tenantId, accountId, accountDTO); + return res.status(200).send({ id: account.id }); + + } catch (error) { + console.log(error); + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Get details of the given account. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + async getAccount(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: accountId } = req.params; + + try { + const account = await this.accountsService.getAccount(tenantId, accountId); + return res.status(200).send({ account }); + + } catch (error) { + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Delete the given account. + * @param {Request} req + * @param {Response} res + * @return {Response} + */ + async deleteAccount(req: Request, res: Response, next: NextFunction) { + const { id: accountId } = req.params; + const { tenantId } = req; + + try { + await this.accountsService.deleteAccount(tenantId, accountId); + return res.status(200).send({ id: accountId }); + + } catch (error) { + console.log(error); + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + + + } + next(); + } + } + + /** + * Activate the given account. + * @param {Response} res - + * @param {Request} req - + * @return {Response} + */ + async activateAccount(req: Request, res: Response, next: Function){ + const { id: accountId } = req.params; + const { tenantId } = req; + + try { + await this.accountsService.activateAccount(tenantId, accountId, true); + return res.status(200).send({ id: accountId }); + } catch (error) { + console.log(error); + + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Inactive the given account. + * @param {Response} res - + * @param {Request} req - + * @return {Response} + */ + async inactivateAccount(req: Request, res: Response, next: Function){ + const { id: accountId } = req.params; + const { tenantId } = req; + + try { + await this.accountsService.activateAccount(tenantId, accountId, false); + return res.status(200).send({ id: accountId }); + + } catch (error) { + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Bulk activate/inactivate accounts. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async bulkToggleActivateAccounts(req: Request, res: Response, next: Function) { + const { type } = req.params; + const { tenantId } = req; + const { ids: accountsIds } = req.query; + + try { + const isActive = (type === 'activate' ? 1 : 0); + await this.accountsService.activateAccounts(tenantId, accountsIds, isActive) + return res.status(200).send({ ids: accountsIds }); + } catch (error) { + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Deletes accounts in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteBulkAccounts(req: Request, res: Response, next: NextFunction) { + const { ids: accountsIds } = req.query; + const { tenantId } = req; + + try { + await this.accountsService.deleteAccounts(tenantId, accountsIds); + return res.status(200).send({ ids: accountsIds }); + + } catch (error) { + console.log(error); + + if (error instanceof ServiceError) { + this.transformServiceErrorToResponse(res, error); + } + next(); + } + } + + /** + * Transforms service errors to response. + * @param {Response} res + * @param {ServiceError} error + */ + transformServiceErrorToResponse(res: Response, error: ServiceError) { + console.log(error.errorType); + if (error.errorType === 'account_not_found') { + return res.boom.notFound( + 'The given account not found.', { + errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }] } + ); + } + if (error.errorType === 'account_type_not_found') { + return res.boom.badRequest( + 'The given account type not found.', { + errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }] } + ); + } + if (error.errorType === 'account_type_not_allowed_to_changed') { + return res.boom.badRequest( + 'Not allowed to change account type of the account.', + { errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }] } + ); + } + if (error.errorType === 'parent_account_not_found') { + return res.boom.badRequest( + 'The parent account not found.', + { errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }] }, + ); + } + if (error.errorType === 'parent_has_different_type') { + return res.boom.badRequest( + 'The parent account has different type.', + { errors: [{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 }] } + ); + } + if (error.errorType === 'account_code_not_unique') { + return res.boom.badRequest( + 'The given account code is not unique.', + { errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }] } + ); + } + if (error.errorType === 'account_has_children') { + return res.boom.badRequest( + 'You could not delete account has children.', + { errors: [{ type: 'ACCOUNT.HAS.CHILD.ACCOUNTS', code: 700 }] } + ); + } + if (error.errorType === 'account_has_associated_transactions') { + return res.boom.badRequest( + 'You could not delete account has associated transactions.', + { errors: [{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 }] } + ); + } + if (error.errorType === 'account_predefined') { + return res.boom.badRequest( + 'You could not delete predefined account', + { errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }] } + ); + } + if (error.errorType === 'accounts_not_found') { + return res.boom.notFound( + 'Some of the given accounts not found.', + { errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }] }, + ); + } + if (error.errorType === 'predefined_accounts') { + return res.boom.badRequest( + 'Some of the given accounts are predefined.', + { errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] } + ); + } + } + + + // /** + // * Retrieve accounts list. + // */ + // getAccountsList(req, res) { + // const validationErrors = validationResult(req); + + // if (!validationErrors.isEmpty()) { + // return res.boom.badData(null, { + // code: 'validation_error', + // ...validationErrors, + // }); + // } + // const filter = { + // display_type: 'flat', + // account_types: [], + // filter_roles: [], + // sort_order: 'asc', + // ...req.query, + // }; + // if (filter.stringified_filter_roles) { + // filter.filter_roles = JSON.parse(filter.stringified_filter_roles); + // } + // const { Resource, Account, View } = req.models; + // const errorReasons = []; + + // const accountsResource = await Resource.query() + // .remember() + // .where('name', 'accounts') + // .withGraphFetched('fields') + // .first(); + + // if (!accountsResource) { + // return res.status(400).send({ + // errors: [{ type: 'ACCOUNTS_RESOURCE_NOT_FOUND', code: 200 }], + // }); + // } + // const resourceFieldsKeys = accountsResource.fields.map((c) => c.key); + + // const view = await View.query().onBuild((builder) => { + // if (filter.custom_view_id) { + // builder.where('id', filter.custom_view_id); + // } else { + // builder.where('favourite', true); + // } + // // builder.where('resource_id', accountsResource.id); + // builder.withGraphFetched('roles.field'); + // builder.withGraphFetched('columns'); + // builder.first(); + + // builder.remember(); + // }); + // const dynamicFilter = new DynamicFilter(Account.tableName); + + // if (filter.column_sort_by) { + // if (resourceFieldsKeys.indexOf(filter.column_sort_by) === -1) { + // errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 }); + // } + // const sortByFilter = new DynamicFilterSortBy( + // filter.column_sort_by, + // filter.sort_order + // ); + // dynamicFilter.setFilter(sortByFilter); + // } + // // View roles. + // if (view && view.roles.length > 0) { + // const viewFilter = new DynamicFilterViews( + // mapViewRolesToConditionals(view.roles), + // view.rolesLogicExpression + // ); + // if (!viewFilter.validateFilterRoles()) { + // errorReasons.push({ + // type: 'VIEW.LOGIC.EXPRESSION.INVALID', + // code: 400, + // }); + // } + // dynamicFilter.setFilter(viewFilter); + // } + // // Filter roles. + // if (filter.filter_roles.length > 0) { + // // Validate the accounts resource fields. + // const filterRoles = new DynamicFilterFilterRoles( + // mapFilterRolesToDynamicFilter(filter.filter_roles), + // accountsResource.fields + // ); + // dynamicFilter.setFilter(filterRoles); + + // if (filterRoles.validateFilterRoles().length > 0) { + // errorReasons.push({ + // type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', + // code: 500, + // }); + // } + // } + // if (errorReasons.length > 0) { + // return res.status(400).send({ errors: errorReasons }); + // } + + // const accounts = await Account.query().onBuild((builder) => { + // builder.modify('filterAccountTypes', filter.account_types); + // builder.withGraphFetched('type'); + // builder.withGraphFetched('balance'); + + // dynamicFilter.buildQuery()(builder); + // }); + // return res.status(200).send({ + // accounts: + // filter.display_type === 'tree' + // ? Account.toNestedArray(accounts) + // : accounts, + // ...(view + // ? { + // customViewId: view.id, + // } + // : {}), + // }); + // } + + // /** + // * Re-calculates balance of the given account. + // */ + // recalcualteBalanace: { + // validation: [param('id').isNumeric().toInt()], + // async handler(req, res) { + // const { id } = req.params; + // const { Account, AccountTransaction, AccountBalance } = req.models; + // const account = await Account.findById(id); + + // if (!account) { + // return res.status(400).send({ + // errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }], + // }); + // } + // const accountTransactions = AccountTransaction.query().where( + // 'account_id', + // account.id + // ); + + // const journalEntries = new JournalPoster(); + // journalEntries.loadFromCollection(accountTransactions); + + // // Delete the balance of the given account id. + // await AccountBalance.query().where('account_id', account.id).delete(); + + // // Save calcualted account balance. + // await journalEntries.saveBalance(); + + // return res.status(200).send(); + // }, + // }, + + + + // /** + // * Transfer all journal entries of the given account to another account. + // */ + // transferToAnotherAccount: { + // validation: [ + // param('id').exists().isNumeric().toInt(), + // param('toAccount').exists().isNumeric().toInt(), + // ], + // async handler(req, res) { + // const validationErrors = validationResult(req); + + // if (!validationErrors.isEmpty()) { + // return res.boom.badData(null, { + // code: 'validation_error', + // ...validationErrors, + // }); + // } + + // // const { id, toAccount: toAccountId } = req.params; + + // // const [fromAccount, toAccount] = await Promise.all([ + // // Account.query().findById(id), + // // Account.query().findById(toAccountId), + // // ]); + + // // const fromAccountTransactions = await AccountTransaction.query() + // // .where('account_id', fromAccount); + + // // return res.status(200).send(); + // }, + // }, + + +}; diff --git a/server/src/http/controllers/Settings.ts b/server/src/http/controllers/Settings.ts index e7b968e88..90c8b8676 100644 --- a/server/src/http/controllers/Settings.ts +++ b/server/src/http/controllers/Settings.ts @@ -47,6 +47,8 @@ export default class SettingsController extends BaseController{ /** * Saves the given options to the storage. + * @param {Request} req - + * @param {Response} res - */ saveSettings(req: Request, res: Response) { const { Option } = req.models; @@ -72,7 +74,11 @@ export default class SettingsController extends BaseController{ settings.set({ ...option }); }); - return res.status(200).send({ }); + return res.status(200).send({ + type: 'success', + code: 'OPTIONS.SAVED.SUCCESSFULLY', + message: 'Options have been saved successfully.', + }); } /** diff --git a/server/src/http/index.js b/server/src/http/index.js index ea2c1a45f..0209bb39a 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -62,8 +62,8 @@ export default () => { dashboard.use('/users', Container.get(Users).router()); dashboard.use('/invite', Container.get(InviteUsers).authRouter()); dashboard.use('/currencies', Currencies.router()); - dashboard.use('/accounts', Accounts.router()); - dashboard.use('/account_types', AccountTypes.router()); + dashboard.use('/accounts', Container.get(Accounts).router()); + dashboard.use('/account_types', Container.get(AccountTypes).router()); dashboard.use('/accounting', Accounting.router()); dashboard.use('/views', Views.router()); dashboard.use('/items', Container.get(Items).router()); diff --git a/server/src/http/middleware/LoggerMiddleware.ts b/server/src/http/middleware/LoggerMiddleware.ts index e9688379f..e57952cd3 100644 --- a/server/src/http/middleware/LoggerMiddleware.ts +++ b/server/src/http/middleware/LoggerMiddleware.ts @@ -1,7 +1,10 @@ import { NextFunction, Request } from 'express'; +import { Container } from 'typedi'; function loggerMiddleware(request: Request, response: Response, next: NextFunction) { - console.log(`${request.method} ${request.path}`); + const Logger = Container.get('logger'); + + Logger.info(`[routes] ${request.method} ${request.path}`); next(); } diff --git a/server/src/loaders/express.ts b/server/src/loaders/express.ts index f5523a1a6..254ef6cd8 100644 --- a/server/src/loaders/express.ts +++ b/server/src/loaders/express.ts @@ -5,6 +5,7 @@ import errorHandler from 'errorhandler'; import fileUpload from 'express-fileupload'; import i18n from 'i18n'; import routes from '@/http'; +import LoggerMiddleware from '@/http/middleware/LoggerMiddleware'; import config from '@/../config/config'; export default ({ app }) => { @@ -29,7 +30,10 @@ export default ({ app }) => { })); // Initialize i18n node. - app.use(i18n.init) + app.use(i18n.init); + + // Logger middleware. + app.use(LoggerMiddleware); // Prefix all application routes. app.use(config.api.prefix, routes()); diff --git a/server/src/services/Accounts/AccountsService.ts b/server/src/services/Accounts/AccountsService.ts index a45f21adf..47d9ef9be 100644 --- a/server/src/services/Accounts/AccountsService.ts +++ b/server/src/services/Accounts/AccountsService.ts @@ -1,29 +1,403 @@ import { Inject, Service } from 'typedi'; import TenancyService from '@/services/Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { IAccountDTO, IAccount } from '@/interfaces'; +import { difference } from 'lodash'; +import { tenant } from 'config/config'; @Service() export default class AccountsService { @Inject() tenancy: TenancyService; - async isAccountExists(tenantId: number, accountId: number) { + @Inject('logger') + logger: any; + + /** + * Retrieve account type or throws service error. + * @param {number} tenantId - + * @param {number} accountTypeId - + * @return {IAccountType} + */ + private async getAccountTypeOrThrowError(tenantId: number, accountTypeId: number) { + const { AccountType } = this.tenancy.models(tenantId); + + this.logger.info('[accounts] validating account type existance.', { tenantId, accountTypeId }); + const accountType = await AccountType.query().findById(accountTypeId); + + if (!accountType) { + this.logger.info('[accounts] account type not found.'); + throw new ServiceError('account_type_not_found'); + } + return accountType; + } + + /** + * Retrieve parent account or throw service error. + * @param {number} tenantId + * @param {number} accountId + * @param {number} notAccountId + */ + private async getParentAccountOrThrowError(tenantId: number, accountId: number, notAccountId?: number) { const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[accounts] validating parent account existance.', { + tenantId, accountId, notAccountId, + }); + const parentAccount = await Account.query().findById(accountId) + .onBuild((query) => { + if (notAccountId) { + query.whereNot('id', notAccountId); + } + }); + if (!parentAccount) { + this.logger.info('[accounts] parent account not found.', { tenantId, accountId }); + throw new ServiceError('parent_account_not_found'); + } + return parentAccount; + } + + /** + * Throws error if the account type was not unique on the storage. + * @param {number} tenantId + * @param {string} accountCode + * @param {number} notAccountId + */ + private async isAccountCodeUniqueOrThrowError(tenantId: number, accountCode: string, notAccountId?: number) { + const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[accounts] validating the account code unique on the storage.', { + tenantId, accountCode, notAccountId, + }); + const account = await Account.query().where('code', accountCode) + .onBuild((query) => { + if (notAccountId) { + query.whereNot('id', notAccountId); + } + }); + + if (account.length > 0) { + this.logger.info('[accounts] account code is not unique.', { tenantId, accountCode }); + throw new ServiceError('account_code_not_unique'); + } + } + + /** + * Throws service error if parent account has different type. + * @param {IAccountDTO} accountDTO + * @param {IAccount} parentAccount + */ + private throwErrorIfParentHasDiffType(accountDTO: IAccountDTO, parentAccount: IAccount) { + if (accountDTO.accountTypeId !== parentAccount.accountTypeId) { + throw new ServiceError('parent_has_different_type'); + } + } + + /** + * Retrieve account of throw service error in case account not found. + * @param {number} tenantId + * @param {number} accountId + * @return {IAccount} + */ + private async getAccountOrThrowError(tenantId: number, accountId: number) { + const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[accounts] validating the account existance.', { tenantId, accountId }); + const account = await Account.query().findById(accountId); + + if (!account) { + this.logger.info('[accounts] the given account not found.', { accountId }); + throw new ServiceError('account_not_found'); + } + return account; + } + + /** + * Diff account type between new and old account, throw service error + * if they have different account type. + * + * @param {IAccount|IAccountDTO} oldAccount + * @param {IAccount|IAccountDTO} newAccount + */ + private async isAccountTypeChangedOrThrowError( + oldAccount: IAccount|IAccountDTO, + newAccount: IAccount|IAccountDTO, + ) { + if (oldAccount.accountTypeId !== newAccount.accountTypeId) { + throw new ServiceError('account_type_not_allowed_to_changed'); + } + } + + /** + * Creates a new account on the storage. + * @param {number} tenantId + * @param {IAccount} accountDTO + * @returns {IAccount} + */ + public async newAccount(tenantId: number, accountDTO: IAccountDTO) { + const { Account } = this.tenancy.models(tenantId); + + if (accountDTO.code) { + await this.isAccountCodeUniqueOrThrowError(tenantId, accountDTO.code); + } + await this.getAccountTypeOrThrowError(tenantId, accountDTO.accountTypeId); + + if (accountDTO.parentAccountId) { + const parentAccount = await this.getParentAccountOrThrowError( + tenantId, accountDTO.parentAccountId + ); + this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); + } + const account = await Account.query().insertAndFetch({ + ...accountDTO, + }); + this.logger.info('[account] account created successfully.', { account, accountDTO }); + return account; + } + + /** + * Edits details of the given account. + * @param {number} tenantId + * @param {number} accountId + * @param {IAccountDTO} accountDTO + */ + public async editAccount(tenantId: number, accountId: number, accountDTO: IAccountDTO) { + const { Account } = this.tenancy.models(tenantId); + const oldAccount = await this.getAccountOrThrowError(tenantId, accountId); + + await this.isAccountTypeChangedOrThrowError(oldAccount, accountDTO); + + // Validate the account code not exists on the storage. + if (accountDTO.code && accountDTO.code !== oldAccount.code) { + await this.isAccountCodeUniqueOrThrowError( + tenantId, + accountDTO.code, + oldAccount.id + ); + } + if (accountDTO.parentAccountId) { + const parentAccount = await this.getParentAccountOrThrowError( + accountDTO.parentAccountId, oldAccount.id, + ); + this.throwErrorIfParentHasDiffType(accountDTO, parentAccount); + } + // Update the account on the storage. + const account = await Account.query().patchAndFetchById( + oldAccount.id, { ...accountDTO } + ); + this.logger.info('[account] account edited successfully.', { + account, accountDTO, tenantId + }); + return account; + } + + /** + * Retrieve the given account details. + * @param {number} tenantId + * @param {number} accountId + */ + public async getAccount(tenantId: number, accountId: number) { + return this.getAccountOrThrowError(tenantId, accountId); + } + + /** + * Detarmine if the given account id exists on the storage. + * @param {number} tenantId + * @param {number} accountId + */ + public async isAccountExists(tenantId: number, accountId: number) { + const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[account] validating the account existance.', { tenantId, accountId }); const foundAccounts = await Account.query() .where('id', accountId); return foundAccounts.length > 0; } - async getAccountByType(tenantId: number, accountTypeKey: string) { + public async getAccountByType(tenantId: number, accountTypeKey: string) { const { AccountType, Account } = this.tenancy.models(tenantId); const accountType = await AccountType.query() - .where('key', accountTypeKey) - .first(); + .findOne('key', accountTypeKey); const account = await Account.query() - .where('account_type_id', accountType.id) - .first(); + .findOne('account_type_id', accountType.id); return account; } + + /** + * Throws error if the account was prefined. + * @param {IAccount} account + */ + private throwErrorIfAccountPredefined(account: IAccount) { + if (account.prefined) { + throw new ServiceError('account_predefined'); + } + } + + /** + * Throws error if account has children accounts. + * @param {number} tenantId + * @param {number} accountId + */ + private async throwErrorIfAccountHasChildren(tenantId: number, accountId: number) { + const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[account] validating if the account has children.', { + tenantId, accountId, + }); + const childAccounts = await Account.query().where( + 'parent_account_id', + accountId, + ); + if (childAccounts.length > 0) { + throw new ServiceError('account_has_children'); + } + } + + /** + * Throws service error if the account has associated transactions. + * @param {number} tenantId + * @param {number} accountId + */ + private async throwErrorIfAccountHasTransactions(tenantId: number, accountId: number) { + const { AccountTransaction } = this.tenancy.models(tenantId); + const accountTransactions = await AccountTransaction.query().where( + 'account_id', accountId, + ); + if (accountTransactions.length > 0) { + throw new ServiceError('account_has_associated_transactions'); + } + } + + /** + * Deletes the account from the storage. + * @param {number} tenantId + * @param {number} accountId + */ + public async deleteAccount(tenantId: number, accountId: number) { + const { Account } = this.tenancy.models(tenantId); + const account = await this.getAccountOrThrowError(tenantId, accountId); + + this.throwErrorIfAccountPredefined(account); + + await this.throwErrorIfAccountHasChildren(tenantId, accountId); + await this.throwErrorIfAccountHasTransactions(tenantId, accountId); + + await Account.query().deleteById(account.id); + this.logger.info('[account] account has been deleted successfully.', { + tenantId, accountId, + }) + } + + /** + * Retrieve the given accounts details or throw error if one account not exists. + * @param {number} tenantId + * @param {number[]} accountsIds + * @return {IAccount[]} + */ + private async getAccountsOrThrowError(tenantId: number, accountsIds: number[]): IAccount[] { + const { Account } = this.tenancy.models(tenantId); + + this.logger.info('[account] trying to validate accounts not exist.', { tenantId, accountsIds }); + const storedAccounts = await Account.query().whereIn('id', accountsIds); + const storedAccountsIds = storedAccounts.map((account) => account.id); + const notFoundAccounts = difference(accountsIds, storedAccountsIds); + + if (notFoundAccounts.length > 0) { + this.logger.error('[account] accounts not exists on the storage.', { tenantId, notFoundAccounts }); + throw new ServiceError('accounts_not_found'); + } + return storedAccounts; + } + + private validatePrefinedAccounts(accounts: IAccount[]) { + const predefined = accounts.filter((account: IAccount) => account.predefined); + + if (predefined.length > 0) { + this.logger.error('[accounts] some accounts predefined.', { predefined }); + throw new ServiceError('predefined_accounts'); + } + return predefined; + } + + /** + * Validating the accounts have associated transactions. + * @param {number} tenantId + * @param {number[]} accountsIds + */ + private async validateAccountsHaveTransactions(tenantId: number, accountsIds: number[]) { + const { AccountTransaction } = this.tenancy.models(tenantId); + const accountsTransactions = await AccountTransaction.query() + .whereIn('account_id', accountsIds) + .count('id as transactions_count') + .groupBy('account_id') + .select('account_id'); + + const accountsHasTransactions: number[] = []; + + accountsTransactions.forEach((transaction) => { + if (transaction.transactionsCount > 0) { + accountsHasTransactions.push(transaction.accountId); + } + }); + if (accountsHasTransactions.length > 0) { + throw new ServiceError('accounts_have_transactions'); + } + } + + /** + * Deletes the given accounts in bulk. + * @param {number} tenantId + * @param {number[]} accountsIds + */ + public async deleteAccounts(tenantId: number, accountsIds: number[]) { + const { Account } = this.tenancy.models(tenantId); + const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds); + + this.validatePrefinedAccounts(accounts); + await this.validateAccountsHaveTransactions(tenantId, accountsIds); + await Account.query().whereIn('id', accountsIds).delete(); + + this.logger.info('[account] given accounts deleted in bulk successfully.', { + tenantId, accountsIds + }); + } + + /** + * Activate accounts in bulk. + * @param {number} tenantId + * @param {number[]} accountsIds + * @param {boolean} activate + */ + public async activateAccounts(tenantId: number, accountsIds: number[], activate: boolean = true) { + const { Account } = this.tenancy.models(tenantId); + const accounts = await this.getAccountsOrThrowError(tenantId, accountsIds); + + this.logger.info('[account] trying activate/inactive the given accounts ids.', { accountsIds }); + await Account.query().whereIn('id', accountsIds) + .patch({ + active: activate ? 1 : 0, + }); + this.logger.info('[account] accounts have been activated successfully.', { tenantId, accountsIds }); + } + + /** + * Activates/Inactivates the given account. + * @param {number} tenantId + * @param {number} accountId + * @param {boolean} activate + */ + public async activateAccount(tenantId: number, accountId: number, activate?: boolean) { + const { Account } = this.tenancy.models(tenantId); + const account = await this.getAccountOrThrowError(tenantId, accountId); + + this.logger.info('[account] trying to activate/inactivate the given account id.'); + await Account.query().where('id', accountId) + .patch({ + active: activate ? 1 : 0, + }) + this.logger.info('[account] account have been activated successfully.', { tenantId, accountId }); + } }