feat: Logger middleware.

feat: refactoring accounts service.
This commit is contained in:
Ahmed Bouhuolia
2020-09-06 20:18:41 +02:00
parent df0cf287ff
commit df0f842681
10 changed files with 1046 additions and 711 deletions

View File

@@ -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;

View File

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

View 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,
});
}
};

View File

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

View 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();
// },
// },
};

View File

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

View File

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

View File

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

View File

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

View File

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