diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index 492aeb25e..253cd06c7 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -1,12 +1,13 @@ -import { Inject, Service } from "typedi"; +import { Inject, Service } from 'typedi'; import { check, param, query } from 'express-validator'; import { Router, Request, Response, NextFunction } from 'express'; import asyncMiddleware from 'api/middleware/asyncMiddleware'; -import BaseController from "api/controllers/BaseController"; -import ExpensesService from "services/Expenses/ExpensesService"; +import BaseController from 'api/controllers/BaseController'; +import ExpensesService from 'services/Expenses/ExpensesService'; import { IExpenseDTO } from 'interfaces'; -import { ServiceError } from "exceptions"; +import { ServiceError } from 'exceptions'; import DynamicListingService from 'services/DynamicListing/DynamicListService'; +import { DATATYPES_LENGTH } from 'data/DataTypes'; @Service() export default class ExpensesController extends BaseController { @@ -23,68 +24,60 @@ export default class ExpensesController extends BaseController { const router = Router(); router.post( - '/', [ - ...this.expenseDTOSchema, - ], + '/', + [...this.expenseDTOSchema], this.validationResult, asyncMiddleware(this.newExpense.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); router.post( - '/publish', [ - ...this.bulkSelectSchema, - ], + '/publish', + [...this.bulkSelectSchema], this.bulkPublishExpenses.bind(this), - this.catchServiceErrors, + this.catchServiceErrors ); router.post( - '/:id/publish', [ - ...this.expenseParamSchema, - ], + '/:id/publish', + [...this.expenseParamSchema], this.validationResult, asyncMiddleware(this.publishExpense.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); router.post( - '/:id', [ - ...this.expenseDTOSchema, - ...this.expenseParamSchema, - ], + '/:id', + [...this.expenseDTOSchema, ...this.expenseParamSchema], this.validationResult, asyncMiddleware(this.editExpense.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); router.delete( - '/:id', [ - ...this.expenseParamSchema, - ], + '/:id', + [...this.expenseParamSchema], this.validationResult, asyncMiddleware(this.deleteExpense.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); - router.delete('/', [ - ...this.bulkSelectSchema, - ], + router.delete( + '/', + [...this.bulkSelectSchema], this.validationResult, asyncMiddleware(this.bulkDeleteExpenses.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); router.get( - '/', [ - ...this.expensesListSchema, - ], + '/', + [...this.expensesListSchema], this.validationResult, asyncMiddleware(this.getExpensesList.bind(this)), this.dynamicListService.handlerErrorsToResponse, - this.catchServiceErrors, + this.catchServiceErrors ); router.get( - '/:id', [ - this.expenseParamSchema, - ], + '/:id', + [this.expenseParamSchema], this.validationResult, asyncMiddleware(this.getExpense.bind(this)), - this.catchServiceErrors, + this.catchServiceErrors ); return router; } @@ -94,26 +87,42 @@ export default class ExpensesController extends BaseController { */ get expenseDTOSchema() { return [ - check('reference_no').optional().trim().escape().isLength({ max: 255 }), + check('reference_no') + .optional({ nullable: true }) + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), check('payment_date').exists().isISO8601(), - check('payment_account_id').exists().isNumeric().toInt(), - check('description').optional(), - check('currency_code').optional(), - check('exchange_rate').optional().isNumeric().toFloat(), + check('payment_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('description') + .optional({ nullable: true }) + .isString() + .isLength({ max: DATATYPES_LENGTH.TEXT }), + check('currency_code').optional().isString().isLength({ max: 3 }), + check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(), check('publish').optional().isBoolean().toBoolean(), check('categories').exists().isArray({ min: 1 }), - check('categories.*.index').exists().isNumeric().toInt(), - check('categories.*.expense_account_id').exists().isNumeric().toInt(), + check('categories.*.index') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), + check('categories.*.expense_account_id') + .exists() + .isInt({ max: DATATYPES_LENGTH.INT_10 }) + .toInt(), check('categories.*.amount') .optional({ nullable: true }) - .isNumeric() - .isDecimal() - .isFloat({ max: 9999999999.999 }) // 13, 3 + .isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3 .toFloat(), - check('categories.*.description').optional().trim().escape().isLength({ - max: 255, - }), + check('categories.*.description') + .optional() + .trim() + .escape() + .isLength({ max: DATATYPES_LENGTH.STRING }), ]; } @@ -121,9 +130,7 @@ export default class ExpensesController extends BaseController { * Expense param schema. */ get expenseParamSchema() { - return [ - param('id').exists().isNumeric().toInt(), - ]; + return [param('id').exists().isNumeric().toInt()]; } get bulkSelectSchema() { @@ -148,16 +155,20 @@ export default class ExpensesController extends BaseController { /** * Creates a new expense on - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async newExpense(req: Request, res: Response, next: NextFunction) { const expenseDTO: IExpenseDTO = this.matchedBodyData(req); const { tenantId, user } = req; try { - const expense = await this.expensesService.newExpense(tenantId, expenseDTO, user); + const expense = await this.expensesService.newExpense( + tenantId, + expenseDTO, + user + ); return res.status(200).send({ id: expense.id }); } catch (error) { next(error); @@ -166,9 +177,9 @@ export default class ExpensesController extends BaseController { /** * Edits details of the given expense. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async editExpense(req: Request, res: Response, next: NextFunction) { const { id: expenseId } = req.params; @@ -176,18 +187,23 @@ export default class ExpensesController extends BaseController { const { tenantId, user } = req; try { - await this.expensesService.editExpense(tenantId, expenseId, expenseDTO, user); + await this.expensesService.editExpense( + tenantId, + expenseId, + expenseDTO, + user + ); return res.status(200).send({ id: expenseId }); } catch (error) { - next(error) + next(error); } } /** * Deletes the given expense. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async deleteExpense(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; @@ -195,25 +211,33 @@ export default class ExpensesController extends BaseController { try { await this.expensesService.deleteExpense(tenantId, expenseId, user); - return res.status(200).send({ id: expenseId }); + + return res.status(200).send({ + id: expenseId, + message: 'The expense has been deleted.', + }); } catch (error) { - next(error) + next(error); } } /** * Publishs the given expense. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async publishExpense(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const { id: expenseId } = req.params; try { - await this.expensesService.publishExpense(tenantId, expenseId, user) - return res.status(200).send({ }); + await this.expensesService.publishExpense(tenantId, expenseId, user); + + return res.status(200).send({ + id: expenseId, + message: 'The expense has been published', + }); } catch (error) { next(error); } @@ -221,16 +245,20 @@ export default class ExpensesController extends BaseController { /** * Deletes the expenses in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async bulkDeleteExpenses(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const { ids: expensesIds } = req.params; try { - await this.expensesService.deleteBulkExpenses(tenantId, expensesIds, user); + await this.expensesService.deleteBulkExpenses( + tenantId, + expensesIds, + user + ); return res.status(200).send({ ids: expensesIds }); } catch (error) { next(error); @@ -239,16 +267,20 @@ export default class ExpensesController extends BaseController { /** * Publishes the given expenses in bulk. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async bulkPublishExpenses(req: Request, res: Response, next: NextFunction) { const { tenantId, user } = req; const { ids: expensesIds } = req.query; try { - await this.expensesService.publishBulkExpenses(tenantId, expensesIds, user); + await this.expensesService.publishBulkExpenses( + tenantId, + expensesIds, + user + ); return res.status(200).send({}); } catch (error) { next(error); @@ -257,8 +289,8 @@ export default class ExpensesController extends BaseController { /** * Retrieve expneses list. - * @param {Request} req - * @param {Response} res + * @param {Request} req + * @param {Response} res * @param {NextFunction} next */ async getExpensesList(req: Request, res: Response, next: NextFunction) { @@ -276,7 +308,11 @@ export default class ExpensesController extends BaseController { } try { - const { expenses, pagination, filterMeta } = await this.expensesService.getExpensesList(tenantId, filter); + const { + expenses, + pagination, + filterMeta, + } = await this.expensesService.getExpensesList(tenantId, filter); return res.status(200).send({ expenses, @@ -290,16 +326,19 @@ export default class ExpensesController extends BaseController { /** * Retrieve expense details. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ async getExpense(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { id: expenseId } = req.params; try { - const expense = await this.expensesService.getExpense(tenantId, expenseId); + const expense = await this.expensesService.getExpense( + tenantId, + expenseId + ); return res.status(200).send({ expense }); } catch (error) { next(error); @@ -308,47 +347,52 @@ export default class ExpensesController extends BaseController { /** * Transform service errors to api response errors. - * @param {Response} res - * @param {ServiceError} error + * @param {Response} res + * @param {ServiceError} error */ - catchServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + catchServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'expense_not_found') { - return res.boom.badRequest( - 'Expense not found.', - { errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }] } - ); + return res.boom.badRequest('Expense not found.', { + errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }], + }); } if (error.errorType === 'total_amount_equals_zero') { - return res.boom.badRequest( - 'Expense total should not equal zero.', - { errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }] }, - ); + return res.boom.badRequest('Expense total should not equal zero.', { + errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }], + }); } if (error.errorType === 'payment_account_not_found') { - return res.boom.badRequest( - 'Payment account not found.', - { errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }] }, - ); + return res.boom.badRequest('Payment account not found.', { + errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }], + }); } if (error.errorType === 'some_expenses_not_found') { - return res.boom.badRequest( - 'Some expense accounts not found.', - { errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }] }, - ); + return res.boom.badRequest('Some expense accounts not found.', { + errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }], + }); } if (error.errorType === 'payment_account_has_invalid_type') { - return res.boom.badRequest( - 'Payment account has invalid type.', - { errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], }, - ); + return res.boom.badRequest('Payment account has invalid type.', { + errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }], + }); } if (error.errorType === 'expenses_account_has_invalid_type') { return res.boom.badRequest(null, { - errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE', code: 600 }] + errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE', code: 600 }], + }); + } + if (error.errorType === 'expense_already_published') { + return res.boom.badRequest(null, { + errors: [{ type: 'EXPENSE_ALREADY_PUBLISHED', code: 700 }], }); } } next(error); } -} \ No newline at end of file +} diff --git a/server/src/services/Expenses/ExpensesService.ts b/server/src/services/Expenses/ExpensesService.ts index caefe22f5..f285bd580 100644 --- a/server/src/services/Expenses/ExpensesService.ts +++ b/server/src/services/Expenses/ExpensesService.ts @@ -20,7 +20,7 @@ const ERRORS = { TOTAL_AMOUNT_EQUALS_ZERO: 'total_amount_equals_zero', PAYMENT_ACCOUNT_HAS_INVALID_TYPE: 'payment_account_has_invalid_type', EXPENSES_ACCOUNT_HAS_INVALID_TYPE: 'expenses_account_has_invalid_type', - EXPENSE_ACCOUNT_ALREADY_PUBLISED: 'expense_already_published', + EXPENSE_ALREADY_PUBLISHED: 'expense_already_published', }; @Service() @@ -219,7 +219,7 @@ export default class ExpensesService implements IExpensesService { */ private validateExpenseIsNotPublished(expense: IExpense) { if (expense.publishedAt) { - throw new ServiceError(ERRORS.EXPENSE_ACCOUNT_ALREADY_PUBLISED); + throw new ServiceError(ERRORS.EXPENSE_ALREADY_PUBLISHED); } }