mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 13:50:31 +00:00
feat: Logger middleware.
feat: refactoring accounts service.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
31
server/src/http/controllers/AccountTypes.ts
Normal file
31
server/src/http/controllers/AccountTypes.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
};
|
||||
617
server/src/http/controllers/Accounts.ts
Normal file
617
server/src/http/controllers/Accounts.ts
Normal file
@@ -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();
|
||||
// },
|
||||
// },
|
||||
|
||||
|
||||
};
|
||||
@@ -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.',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user