mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 21:30:31 +00:00
621 lines
18 KiB
TypeScript
621 lines
18 KiB
TypeScript
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) {
|
|
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) {
|
|
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_name_not_unqiue') {
|
|
return res.boom.badRequest(
|
|
'The given account not unique.',
|
|
{ errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }], }
|
|
);
|
|
}
|
|
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();
|
|
// },
|
|
// },
|
|
|
|
|
|
};
|