From 933afb37bf1c460b73b9f98f6e6033af8791dd9e Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Sat, 26 Sep 2020 16:23:57 +0200 Subject: [PATCH] refactor: currencies service. refactor: exchange rates service. --- server/src/api/controllers/Accounts.ts | 4 + server/src/api/controllers/BaseController.ts | 19 ++ .../src/api/controllers/Contacts/Contacts.ts | 23 +- .../src/api/controllers/Contacts/Customers.ts | 28 +-- .../src/api/controllers/Contacts/Vendors.ts | 40 ++-- server/src/api/controllers/Currencies.js | 135 ----------- server/src/api/controllers/Currencies.ts | 180 +++++++++++++++ server/src/api/controllers/ExchangeRates.js | 209 ----------------- server/src/api/controllers/ExchangeRates.ts | 217 ++++++++++++++++++ server/src/api/controllers/Expenses.ts | 7 +- server/src/api/controllers/InviteUsers.ts | 6 +- server/src/api/index.ts | 5 +- server/src/interfaces/Currency.ts | 24 ++ server/src/interfaces/ExchangeRate.ts | 32 +++ server/src/interfaces/ManualJournal.ts | 1 - server/src/interfaces/index.ts | 4 +- server/src/models/Currency.js | 2 +- server/src/models/ExchangeRate.js | 2 +- server/src/services/Authentication/index.ts | 2 +- .../src/services/Contacts/ContactsService.ts | 2 +- .../services/Currencies/CurrenciesService.ts | 152 ++++++++++++ .../ExchangeRates/ExchangeRatesService.ts | 166 ++++++++++++++ server/src/services/Tenancy/TenantsManager.ts | 2 +- server/src/subscribers/events.ts | 15 ++ .../repositories/SubscriptionRepository.ts | 2 +- 25 files changed, 878 insertions(+), 401 deletions(-) delete mode 100644 server/src/api/controllers/Currencies.js create mode 100644 server/src/api/controllers/Currencies.ts delete mode 100644 server/src/api/controllers/ExchangeRates.js create mode 100644 server/src/api/controllers/ExchangeRates.ts create mode 100644 server/src/interfaces/Currency.ts create mode 100644 server/src/interfaces/ExchangeRate.ts create mode 100644 server/src/services/Currencies/CurrenciesService.ts create mode 100644 server/src/services/ExchangeRates/ExchangeRatesService.ts diff --git a/server/src/api/controllers/Accounts.ts b/server/src/api/controllers/Accounts.ts index eb9851823..0a34c7c10 100644 --- a/server/src/api/controllers/Accounts.ts +++ b/server/src/api/controllers/Accounts.ts @@ -85,6 +85,7 @@ export default class AccountsController extends BaseController{ router.delete( '/', this.bulkDeleteSchema, + this.validationResult, asyncMiddleware(this.deleteBulkAccounts.bind(this)), this.catchServiceErrors, ); @@ -144,6 +145,9 @@ export default class AccountsController extends BaseController{ ]; } + /** + * + */ get bulkDeleteSchema() { return [ query('ids').isArray({ min: 2 }), diff --git a/server/src/api/controllers/BaseController.ts b/server/src/api/controllers/BaseController.ts index 519c12bea..1ff423c00 100644 --- a/server/src/api/controllers/BaseController.ts +++ b/server/src/api/controllers/BaseController.ts @@ -5,10 +5,19 @@ import { mapKeysDeep } from 'utils' export default class BaseController { + /** + * Converts plain object keys to cameCase style. + * @param {Object} data + */ private dataToCamelCase(data) { return mapKeysDeep(data, (v, k) => camelCase(k)); } + /** + * Matches the body data from validation schema. + * @param {Request} req + * @param options + */ matchedBodyData(req: Request, options: any = {}) { const data = matchedData(req, { locations: ['body'], @@ -18,6 +27,10 @@ export default class BaseController { return this.dataToCamelCase(data); } + /** + * Matches the query data from validation schema. + * @param {Request} req + */ matchedQueryData(req: Request) { const data = matchedData(req, { locations: ['query'], @@ -25,6 +38,12 @@ export default class BaseController { return this.dataToCamelCase(data); } + /** + * Validate validation schema middleware. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ validationResult(req: Request, res: Response, next: NextFunction) { const validationErrors = validationResult(req); diff --git a/server/src/api/controllers/Contacts/Contacts.ts b/server/src/api/controllers/Contacts/Contacts.ts index ccde5c555..8ba52e7b7 100644 --- a/server/src/api/controllers/Contacts/Contacts.ts +++ b/server/src/api/controllers/Contacts/Contacts.ts @@ -1,12 +1,11 @@ -import { check, param, query } from 'express-validator'; +import { check, param, query, ValidationChain } from 'express-validator'; import BaseController from "api/controllers/BaseController"; export default class ContactsController extends BaseController { - /** - * Contact DTO schema. + * @returns {ValidationChain[]} */ - get contactDTOSchema() { + get contactDTOSchema(): ValidationChain[] { return [ check('first_name').optional().trim().escape(), check('last_name').optional().trim().escape(), @@ -39,8 +38,9 @@ export default class ContactsController extends BaseController { /** * Contact new DTO schema. + * @returns {ValidationChain[]} */ - get contactNewDTOSchema() { + get contactNewDTOSchema(): ValidationChain[] { return [ check('balance').optional().isNumeric().toInt(), ]; @@ -48,20 +48,27 @@ export default class ContactsController extends BaseController { /** * Contact edit DTO schema. + * @returns {ValidationChain[]} */ - get contactEditDTOSchema() { + get contactEditDTOSchema(): ValidationChain[] { return [ ] } - get specificContactSchema() { + /** + * @returns {ValidationChain[]} + */ + get specificContactSchema(): ValidationChain[] { return [ param('id').exists().isNumeric().toInt(), ]; } - get bulkContactsSchema() { + /** + * @returns {ValidationChain[]} + */ + get bulkContactsSchema(): ValidationChain[] { return [ query('ids').isArray({ min: 2 }), query('ids.*').isNumeric().toInt(), diff --git a/server/src/api/controllers/Contacts/Customers.ts b/server/src/api/controllers/Contacts/Customers.ts index 8db9af516..b271cf7f9 100644 --- a/server/src/api/controllers/Contacts/Customers.ts +++ b/server/src/api/controllers/Contacts/Customers.ts @@ -19,36 +19,36 @@ export default class CustomersController extends ContactsController { const router = Router(); router.post('/', [ - ...this.contactDTOSchema, - ...this.contactNewDTOSchema, - ...this.customerDTOSchema, - ], + ...this.contactDTOSchema, + ...this.contactNewDTOSchema, + ...this.customerDTOSchema, + ], this.validationResult, asyncMiddleware(this.newCustomer.bind(this)) ); router.post('/:id', [ - ...this.contactDTOSchema, - ...this.contactEditDTOSchema, - ...this.customerDTOSchema, - ], + ...this.contactDTOSchema, + ...this.contactEditDTOSchema, + ...this.customerDTOSchema, + ], this.validationResult, asyncMiddleware(this.editCustomer.bind(this)) ); router.delete('/:id', [ - ...this.specificContactSchema, - ], + ...this.specificContactSchema, + ], this.validationResult, asyncMiddleware(this.deleteCustomer.bind(this)) ); router.delete('/', [ - ...this.bulkContactsSchema, - ], + ...this.bulkContactsSchema, + ], this.validationResult, asyncMiddleware(this.deleteBulkCustomers.bind(this)) ); router.get('/:id', [ - ...this.specificContactSchema, - ], + ...this.specificContactSchema, + ], this.validationResult, asyncMiddleware(this.getCustomer.bind(this)) ); diff --git a/server/src/api/controllers/Contacts/Vendors.ts b/server/src/api/controllers/Contacts/Vendors.ts index e97683327..95f848fdd 100644 --- a/server/src/api/controllers/Contacts/Vendors.ts +++ b/server/src/api/controllers/Contacts/Vendors.ts @@ -1,6 +1,6 @@ import { Request, Response, Router, NextFunction } from 'express'; import { Service, Inject } from 'typedi'; -import { check, query } from 'express-validator'; +import { check, query, ValidationChain } from 'express-validator'; import ContactsController from 'api/controllers/Contacts/Contacts'; import VendorsService from 'services/Contacts/VendorsService'; import { ServiceError } from 'exceptions'; @@ -19,42 +19,42 @@ export default class VendorsController extends ContactsController { const router = Router(); router.post('/', [ - ...this.contactDTOSchema, - ...this.contactNewDTOSchema, - ...this.vendorDTOSchema, - ], + ...this.contactDTOSchema, + ...this.contactNewDTOSchema, + ...this.vendorDTOSchema, + ], this.validationResult, asyncMiddleware(this.newVendor.bind(this)) ); router.post('/:id', [ - ...this.contactDTOSchema, - ...this.contactEditDTOSchema, - ...this.vendorDTOSchema, - ], + ...this.contactDTOSchema, + ...this.contactEditDTOSchema, + ...this.vendorDTOSchema, + ], this.validationResult, asyncMiddleware(this.editVendor.bind(this)) ); router.delete('/:id', [ - ...this.specificContactSchema, - ], + ...this.specificContactSchema, + ], this.validationResult, asyncMiddleware(this.deleteVendor.bind(this)) ); router.delete('/', [ - ...this.bulkContactsSchema, - ], + ...this.bulkContactsSchema, + ], this.validationResult, asyncMiddleware(this.deleteBulkVendors.bind(this)) ); router.get('/:id', [ - ...this.specificContactSchema, - ], + ...this.specificContactSchema, + ], this.validationResult, asyncMiddleware(this.getVendor.bind(this)) ); router.get('/', [ - ...this.vendorsListSchema, - ], + ...this.vendorsListSchema, + ], this.validationResult, asyncMiddleware(this.getVendorsList.bind(this)), ); @@ -63,8 +63,9 @@ export default class VendorsController extends ContactsController { /** * Vendor DTO schema. + * @returns {ValidationChain[]} */ - get vendorDTOSchema() { + get vendorDTOSchema(): ValidationChain[] { return [ check('opening_balance').optional().isNumeric().toInt(), ]; @@ -72,6 +73,7 @@ export default class VendorsController extends ContactsController { /** * Vendors datatable list validation schema. + * @returns {ValidationChain[]} */ get vendorsListSchema() { return [ @@ -231,7 +233,7 @@ export default class VendorsController extends ContactsController { const vendors = await this.vendorsService.getVendorsList(tenantId, vendorsFilter); return res.status(200).send({ vendors }); } catch (error) { - + next(error); } } } \ No newline at end of file diff --git a/server/src/api/controllers/Currencies.js b/server/src/api/controllers/Currencies.js deleted file mode 100644 index ddafd824c..000000000 --- a/server/src/api/controllers/Currencies.js +++ /dev/null @@ -1,135 +0,0 @@ -import express from 'express'; -import { check, param, validationResult } from 'express-validator'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; - -export default { - /** - * Router constructor. - */ - router() { - const router = express.Router(); - - router.get('/', - this.all.validation, - asyncMiddleware(this.all.handler)); - - router.post('/', - this.newCurrency.validation, - asyncMiddleware(this.newCurrency.handler)); - - router.post('/:id', - this.editCurrency.validation, - asyncMiddleware(this.editCurrency.handler)); - - router.delete('/:currency_code', - this.deleteCurrecy.validation, - asyncMiddleware(this.deleteCurrecy.handler)); - - return router; - }, - - /** - * Retrieve all registered currency details. - */ - all: { - validation: [], - async handler(req, res) { - const { Currency } = req.models; - const currencies = await Currency.query(); - - return res.status(200).send({ - currencies: [ - ...currencies, - ], - }); - }, - }, - - newCurrency: { - validation: [ - check('currency_name').exists().trim().escape(), - check('currency_code').exists().trim().escape(), - ], - 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 { Currency } = req.models; - - const foundCurrency = await Currency.query() - .where('currency_code', form.currency_code); - - if (foundCurrency.length > 0) { - return res.status(400).send({ - errors: [{ type: 'CURRENCY.CODE.ALREADY.EXISTS', code: 100 }], - }); - } - await Currency.query() - .insert({ ...form }); - - return res.status(200).send({ - currency: { ...form }, - }); - }, - }, - - deleteCurrecy: { - validation: [ - param('currency_code').exists().trim().escape(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { Currency } = req.models; - const { currency_code: currencyCode } = req.params; - - await Currency.query() - .where('currency_code', currencyCode) - .delete(); - - return res.status(200).send({ currency_code: currencyCode }); - }, - }, - - editCurrency: { - validation: [ - param('id').exists().isNumeric().toInt(), - check('currency_name').exists().trim().escape(), - check('currency_code').exists().trim().escape(), - ], - 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 { id } = req.params; - const { Currency } = req.models; - - const foundCurrency = await Currency.query() - .where('currency_code', form.currency_code).whereNot('id', id); - - if (foundCurrency.length > 0) { - return res.status(400).send({ - errors: [{ type: 'CURRENCY.CODE.ALREADY.EXISTS', code: 100 }], - }); - } - await Currency.query().where('id', id).update({ ...form }); - - return res.status(200).send({ currency: { ...form } }); - }, - }, -}; diff --git a/server/src/api/controllers/Currencies.ts b/server/src/api/controllers/Currencies.ts new file mode 100644 index 000000000..06f0fbb5e --- /dev/null +++ b/server/src/api/controllers/Currencies.ts @@ -0,0 +1,180 @@ +import { Router, Request, Response, NextFunction } from 'express'; +import { check, param, query, ValidationChain } from 'express-validator'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from './BaseController'; +import CurrenciesService from 'services/Currencies/CurrenciesService'; +import { Inject, Service } from 'typedi'; +import { ServiceError } from 'exceptions'; + +@Service() +export default class CurrenciesController extends BaseController { + @Inject() + currenciesService: CurrenciesService; + + /** + * Router constructor. + */ + router() { + const router = Router(); + + router.get('/', [ + ...this.listSchema, + ], + this.validationResult, + asyncMiddleware(this.all.bind(this)) + ); + router.post('/', [ + ...this.currencyDTOSchemaValidation, + ], + this.validationResult, + asyncMiddleware(this.newCurrency.bind(this)), + this.handlerServiceError, + ); + router.post('/:id', [ + ...this.currencyIdParamSchema, + ...this.currencyEditDTOSchemaValidation + ], + this.validationResult, + asyncMiddleware(this.editCurrency.bind(this)), + this.handlerServiceError, + ); + router.delete('/:currency_code', [ + ...this.currencyParamSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteCurrency.bind(this)), + this.handlerServiceError, + ); + return router; + } + + get currencyDTOSchemaValidation(): ValidationChain[] { + return [ + check('currency_name').exists().trim().escape(), + check('currency_code').exists().trim().escape(), + ]; + } + + get currencyEditDTOSchemaValidation(): ValidationChain[] { + return [ + check('currency_name').exists().trim().escape(), + ]; + } + + get currencyIdParamSchema(): ValidationChain[] { + return [ + param('id').exists().isNumeric().toInt(), + ]; + } + + get currencyParamSchema(): ValidationChain[] { + return [ + param('currency_code').exists().trim().escape(), + ]; + } + + get listSchema(): ValidationChain[] { + return [ + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + + /** + * Retrieve all registered currency details. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async all(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + + try { + const currencies = await this.currenciesService.listCurrencies(tenantId); + return res.status(200).send({ currencies: [ ...currencies, ] }); + } catch (error) { + next(error); + } + } + + /** + * Creates a new currency on the storage. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async newCurrency(req: Request, res: Response, next: Function) { + const { tenantId } = req; + const currencyDTO = this.matchedBodyData(req); + + try { + await this.currenciesService.newCurrency(tenantId, currencyDTO); + + return res.status(200).send({ + currency_code: currencyDTO.currencyCode, + }); + } catch (error) { + next(error); + } + } + + /** + * Edits details of the given currency. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteCurrency(req: Request, res: Response, next: Function) { + const { tenantId } = req; + const { currency_code: currencyCode } = req.params; + + try { + await this.currenciesService.deleteCurrency(tenantId, currencyCode); + return res.status(200).send({ currency_code: currencyCode }); + } catch (error) { + next(error); + } + } + + /** + * Deletes the currency. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async editCurrency(req: Request, res: Response, next: Function) { + const { tenantId } = req; + const { id: currencyId } = req.params; + const { body: editCurrencyDTO } = req; + + try { + const currency = await this.currenciesService.editCurrency(tenantId, currencyId, editCurrencyDTO); + return res.status(200).send({ currency_code: currency.currencyCode }); + } catch (error) { + next(error); + } + } + + /** + * Handles currencies service error. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handlerServiceError(error, req, res, next) { + if (error instanceof ServiceError) { + if (error.errorType === 'currency_not_found') { + return res.boom.badRequest(null, { + errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100, }], + }); + } + if (error.errorType === 'currency_code_exists') { + return res.boom.badRequest(null, { + errors: [{ type: 'CURRENCY_CODE_EXISTS', code: 200, }], + }); + } + } + next(error); + } +}; diff --git a/server/src/api/controllers/ExchangeRates.js b/server/src/api/controllers/ExchangeRates.js deleted file mode 100644 index 157eec63c..000000000 --- a/server/src/api/controllers/ExchangeRates.js +++ /dev/null @@ -1,209 +0,0 @@ -import express from 'express'; -import { - check, - param, - query, - validationResult, -} from 'express-validator'; -import moment from 'moment'; -import { difference } from 'lodash'; -import asyncMiddleware from 'api/middleware/asyncMiddleware'; - -export default { - /** - * Constructor method. - */ - router() { - const router = express.Router(); - - router.get('/', - this.exchangeRates.validation, - asyncMiddleware(this.exchangeRates.handler)); - - router.post('/', - this.addExchangeRate.validation, - asyncMiddleware(this.addExchangeRate.handler)); - - router.post('/:id', - this.editExchangeRate.validation, - asyncMiddleware(this.editExchangeRate.handler)); - - router.delete('/bulk', - this.bulkDeleteExchangeRates.validation, - asyncMiddleware(this.bulkDeleteExchangeRates.handler)); - - router.delete('/:id', - this.deleteExchangeRate.validation, - asyncMiddleware(this.deleteExchangeRate.handler)); - - return router; - }, - - /** - * Retrieve exchange rates. - */ - exchangeRates: { - validation: [ - query('page').optional().isNumeric().toInt(), - query('page_size').optional().isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const filter = { - page: 1, - page_size: 10, - ...req.query, - }; - const { ExchangeRate } = req.models; - const exchangeRates = await ExchangeRate.query() - .pagination(filter.page - 1, filter.page_size); - - return res.status(200).send({ exchange_rates: exchangeRates }); - }, - }, - - /** - * Adds a new exchange rate on the given date. - */ - addExchangeRate: { - validation: [ - check('exchange_rate').exists().isNumeric().toFloat(), - check('currency_code').exists().trim().escape(), - check('date').exists().isISO8601(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { ExchangeRate } = req.models; - const form = { ...req.body }; - const foundExchangeRate = await ExchangeRate.query() - .where('currency_code', form.currency_code) - .where('date', form.date); - - if (foundExchangeRate.length > 0) { - return res.status(400).send({ - errors: [{ type: 'EXCHANGE.RATE.DATE.PERIOD.DEFINED', code: 200 }], - }); - } - await ExchangeRate.query().insert({ - ...form, - date: moment(form.date).format('YYYY-MM-DD'), - }); - - return res.status(200).send(); - }, - }, - - - /** - * Edit the given exchange rate. - */ - editExchangeRate: { - validation: [ - param('id').exists().isNumeric().toInt(), - check('exchange_rate').exists().isNumeric().toFloat(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { id } = req.params; - const form = { ...req.body }; - const { ExchangeRate } = req.models; - - const foundExchangeRate = await ExchangeRate.query() - .where('id', id); - - if (!foundExchangeRate.length) { - return res.status(400).send({ - errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }], - }); - } - await ExchangeRate.query() - .where('id', id) - .update({ ...form }); - - return res.status(200).send({ id }); - }, - }, - - /** - * Delete the given exchange rate from the storage. - */ - deleteExchangeRate: { - validation: [ - param('id').isNumeric().toInt(), - ], - async handler(req, res) { - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - const { id } = req.params; - const { ExchangeRate } = req.models; - const foundExchangeRate = await ExchangeRate.query().where('id', id); - - if (!foundExchangeRate.length) { - return res.status(404).send({ - errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }], - }); - } - await ExchangeRate.query().where('id', id).delete(); - - return res.status(200).send({ id }); - }, - }, - - bulkDeleteExchangeRates: { - validation: [ - query('ids').isArray({ min: 2 }), - 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 { ExchangeRate } = req.models; - - const exchangeRates = await ExchangeRate.query().whereIn('id', filter.ids); - const exchangeRatesIds = exchangeRates.map((category) => category.id); - const notFoundExRates = difference(filter.ids, exchangeRatesIds); - - if (notFoundExRates.length > 0) { - return res.status(400).send({ - errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 200, ids: notFoundExRates }], - }); - } - await ExchangeRate.query().whereIn('id', exchangeRatesIds).delete(); - - return res.status(200).send({ ids: exchangeRatesIds }); - }, - }, -} \ No newline at end of file diff --git a/server/src/api/controllers/ExchangeRates.ts b/server/src/api/controllers/ExchangeRates.ts new file mode 100644 index 000000000..5bb6912b9 --- /dev/null +++ b/server/src/api/controllers/ExchangeRates.ts @@ -0,0 +1,217 @@ +import { Service, Inject } from 'typedi'; +import { Router, Request, Response, NextFunction } from 'express'; +import { + check, + param, + query, +} from 'express-validator'; +import asyncMiddleware from 'api/middleware/asyncMiddleware'; +import BaseController from './BaseController'; +import { ServiceError } from 'exceptions'; +import ExchangeRatesService from 'services/ExchangeRates/ExchangeRatesService'; + +@Service() +export default class ExchangeRatesController extends BaseController { + + @Inject() + exchangeRatesService: ExchangeRatesService; + + /** + * Constructor method. + */ + router() { + const router = Router(); + + router.get('/', [ + ...this.exchangeRatesListSchema, + ], + this.validationResult, + asyncMiddleware(this.exchangeRates.bind(this)), + this.handleServiceError, + ); + router.post('/', [ + ...this.exchangeRateDTOSchema + ], + this.validationResult, + asyncMiddleware(this.addExchangeRate.bind(this)), + this.handleServiceError, + ); + router.post('/:id', [ + ...this.exchangeRateEditDTOSchema, + ...this.exchangeRateIdSchema, + ], + this.validationResult, + asyncMiddleware(this.editExchangeRate.bind(this)), + this.handleServiceError, + ); + router.delete('/bulk', [ + ...this.exchangeRatesIdsSchema, + ], + this.validationResult, + asyncMiddleware(this.bulkDeleteExchangeRates.bind(this)), + this.handleServiceError, + ); + router.delete('/:id', [ + ...this.exchangeRateIdSchema, + ], + this.validationResult, + asyncMiddleware(this.deleteExchangeRate.bind(this)), + this.handleServiceError, + ); + return router; + } + + get exchangeRatesListSchema() { + return [ + query('page').optional().isNumeric().toInt(), + query('page_size').optional().isNumeric().toInt(), + ]; + } + + get exchangeRateDTOSchema() { + return [ + check('exchange_rate').exists().isNumeric().toFloat(), + check('currency_code').exists().trim().escape(), + check('date').exists().isISO8601(), + ]; + } + + get exchangeRateEditDTOSchema() { + return [ + check('exchange_rate').exists().isNumeric().toFloat(), + ]; + } + + get exchangeRateIdSchema() { + return [ + param('id').isNumeric().toInt(), + ]; + } + + get exchangeRatesIdsSchema() { + return [ + query('ids').isArray({ min: 2 }), + query('ids.*').isNumeric().toInt(), + ]; + } + + /** + * Retrieve exchange rates. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async exchangeRates(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const filter = { + page: 1, + pageSize: 100, + ...req.query, + }; + try { + const exchangeRates = await this.exchangeRatesService.listExchangeRates(tenantId, filter); + return res.status(200).send({ exchange_rates: exchangeRates }); + } catch (error) { + next(error); + } + } + + /** + * Adds a new exchange rate on the given date. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async addExchangeRate(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const exchangeRateDTO = this.matchedBodyData(req); + + try { + const exchangeRate = await this.exchangeRatesService.newExchangeRate(tenantId, exchangeRateDTO) + return res.status(200).send({ id: exchangeRate.id }); + } catch (error) { + next(error); + } + } + + /** + * Edit the given exchange rate. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async editExchangeRate(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const exchangeRateDTO = this.matchedBodyData(req); + + try { + const exchangeRate = await this.exchangeRatesService.newExchangeRate(tenantId, exchangeRateDTO) + return res.status(200).send({ id: exchangeRate.id }); + } catch (error) { + next(error); + } + } + + /** + * Delete the given exchange rate from the storage. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async deleteExchangeRate(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { id: exchangeRateId } = req.params; + + try { + await this.exchangeRatesService.deleteExchangeRate(tenantId, exchangeRateId); + return res.status(200).send({ id: exchangeRateId }); + } catch (error) { + next(error); + } + } + + /** + * Deletes the given exchange rates in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + async bulkDeleteExchangeRates(req: Request, res: Response, next: NextFunction) { + const { tenantId } = req; + const { ids: exchangeRateIds } = req.query; + + try { + await this.exchangeRatesService.deleteBulkExchangeRates(tenantId, exchangeRateIds); + return res.status(200).send(); + } catch (error) { + next(error); + } + } + + /** + * Handle service errors. + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ + handleServiceError(error: Error, req: Request, res: Response, next: NextFunction) { + if (error instanceof ServiceError) { + if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') { + return res.status(404).send({ + errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }], + }); + } + if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') { + return res.status(400).send({ + errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }], + }); + } + if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') { + return res.status(400).send({ + errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }], + }); + } + } + } +} \ No newline at end of file diff --git a/server/src/api/controllers/Expenses.ts b/server/src/api/controllers/Expenses.ts index c69b752bd..fd4480282 100644 --- a/server/src/api/controllers/Expenses.ts +++ b/server/src/api/controllers/Expenses.ts @@ -211,7 +211,12 @@ export default class ExpensesController extends BaseController { } } - + /** + * Publishes the given expenses in bulk. + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next + */ async bulkPublishExpenses(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; diff --git a/server/src/api/controllers/InviteUsers.ts b/server/src/api/controllers/InviteUsers.ts index 1ffc8a065..3f9d4ab10 100644 --- a/server/src/api/controllers/InviteUsers.ts +++ b/server/src/api/controllers/InviteUsers.ts @@ -22,8 +22,8 @@ export default class InviteUsersController extends BaseController { const router = Router(); router.post('/send', [ - body('email').exists().trim().escape(), - ], + body('email').exists().trim().escape(), + ], this.validationResult, asyncMiddleware(this.sendInvite.bind(this)), ); @@ -117,9 +117,7 @@ export default class InviteUsersController extends BaseController { message: 'User invite has been accepted successfully.', }); } catch (error) { - if (error instanceof ServiceError) { - if (error.errorType === 'phone_number_exists') { return res.status(400).send({ errors: [{ type: 'PHONE_NUMBER.EXISTS' }], diff --git a/server/src/api/index.ts b/server/src/api/index.ts index ca728a57e..399e06e91 100644 --- a/server/src/api/index.ts +++ b/server/src/api/index.ts @@ -60,10 +60,9 @@ export default () => { dashboard.use('/users', Container.get(Users).router()); dashboard.use('/invite', Container.get(InviteUsers).authRouter()); - dashboard.use('/currencies', Currencies.router()); + dashboard.use('/currencies', Container.get(Currencies).router()); dashboard.use('/accounts', Container.get(Accounts).router()); dashboard.use('/account_types', Container.get(AccountTypes).router()); - // dashboard.use('/accounting', Accounting.router()); dashboard.use('/manual-journals', Container.get(ManualJournals).router()); dashboard.use('/views', Views.router()); dashboard.use('/items', Container.get(Items).router()); @@ -76,7 +75,7 @@ export default () => { dashboard.use('/vendors', Container.get(Vendors).router()); dashboard.use('/purchases', Purchases.router()); dashboard.use('/resources', Resources.router()); - dashboard.use('/exchange_rates', ExchangeRates.router()); + dashboard.use('/exchange_rates', Container.get(ExchangeRates).router()); dashboard.use('/media', Media.router()) app.use('/', dashboard); diff --git a/server/src/interfaces/Currency.ts b/server/src/interfaces/Currency.ts new file mode 100644 index 000000000..112d62ac5 --- /dev/null +++ b/server/src/interfaces/Currency.ts @@ -0,0 +1,24 @@ + + +export interface ICurrencyDTO { + currencyName: string, + currencyCode: string, +}; +export interface ICurrencyEditDTO { + currencyName: string, +} +export interface ICurrency { + id: number, + currencyName: string, + currencyCode: string, + createdAt: Date, + updatedAt: Date, +}; + +export interface ICurrenciesService { + newCurrency(tenantId: number, currencyDTO: ICurrencyDTO): Promise; + editCurrency(tenantId: number, currencyId: number, editCurrencyDTO: ICurrencyEditDTO): Promise; + + deleteCurrency(tenantId: number, currencyCode: string): Promise; + listCurrencies(tenantId: number): Promise; +} \ No newline at end of file diff --git a/server/src/interfaces/ExchangeRate.ts b/server/src/interfaces/ExchangeRate.ts new file mode 100644 index 000000000..1fbc68e7e --- /dev/null +++ b/server/src/interfaces/ExchangeRate.ts @@ -0,0 +1,32 @@ + +export interface IExchangeRate { + id: number, + currencyCode: string, + exchangeRate: number, + date: Date, + createdAt: Date, + updatedAt: Date, +}; + +export interface IExchangeRateDTO { + currencyCode: string, + exchangeRate: number, + date: Date, +}; + +export interface IExchangeRateEditDTO { + exchangeRate: number, +}; + +export interface IExchangeRateFilter { + page: number, + pageSize: number, +}; + +export interface IExchangeRatesService { + newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise; + editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise; + + deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise; + listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise; +}; \ No newline at end of file diff --git a/server/src/interfaces/ManualJournal.ts b/server/src/interfaces/ManualJournal.ts index 74a36ba65..cc40c3611 100644 --- a/server/src/interfaces/ManualJournal.ts +++ b/server/src/interfaces/ManualJournal.ts @@ -2,7 +2,6 @@ import { IDynamicListFilterDTO } from "./DynamicFilter"; import { IJournalEntry } from "./Journal"; import { ISystemUser } from "./User"; - export interface IManualJournal { id: number, date: Date|string, diff --git a/server/src/interfaces/index.ts b/server/src/interfaces/index.ts index 7738cfa11..17852dea8 100644 --- a/server/src/interfaces/index.ts +++ b/server/src/interfaces/index.ts @@ -20,4 +20,6 @@ export * from './Contact'; export * from './Expenses'; export * from './Tenancy'; export * from './View'; -export * from './ManualJournal'; \ No newline at end of file +export * from './ManualJournal'; +export * from './Currency'; +export * from './ExchangeRate'; \ No newline at end of file diff --git a/server/src/models/Currency.js b/server/src/models/Currency.js index e1c6dbacb..8fa46eb54 100644 --- a/server/src/models/Currency.js +++ b/server/src/models/Currency.js @@ -11,7 +11,7 @@ export default class Currency extends TenantModel { /** * Timestamps columns. */ - static get timestamps() { + get timestamps() { return ['createdAt', 'updatedAt']; } } diff --git a/server/src/models/ExchangeRate.js b/server/src/models/ExchangeRate.js index 48340f8ed..22a17ca01 100644 --- a/server/src/models/ExchangeRate.js +++ b/server/src/models/ExchangeRate.js @@ -13,7 +13,7 @@ export default class ExchangeRate extends TenantModel { /** * Timestamps columns. */ - static get timestamps() { + get timestamps() { return ['createdAt', 'updatedAt']; } } \ No newline at end of file diff --git a/server/src/services/Authentication/index.ts b/server/src/services/Authentication/index.ts index 20414a7ae..1f15dc98d 100644 --- a/server/src/services/Authentication/index.ts +++ b/server/src/services/Authentication/index.ts @@ -49,7 +49,7 @@ export default class AuthenticationService { * @param {string} password - Password. * @return {Promise<{user: IUser, token: string}>} */ - async signIn(emailOrPhone: string, password: string): Promise<{user: IUser, token: string, tenant: ITenant }> { + async signIn(emailOrPhone: string, password: string): Promise<{user: ISystemUser, token: string, tenant: ITenant }> { this.logger.info('[login] Someone trying to login.', { emailOrPhone, password }); const { systemUserRepository } = this.sysRepositories; diff --git a/server/src/services/Contacts/ContactsService.ts b/server/src/services/Contacts/ContactsService.ts index 633f46e6e..0577dd74f 100644 --- a/server/src/services/Contacts/ContactsService.ts +++ b/server/src/services/Contacts/ContactsService.ts @@ -2,7 +2,7 @@ import { Inject, Service } from 'typedi'; import { difference, upperFirst } from 'lodash'; import { ServiceError } from "exceptions"; import TenancyService from 'services/Tenancy/TenancyService'; -import { +import { IContact, IContactNewDTO, IContactEditDTO, diff --git a/server/src/services/Currencies/CurrenciesService.ts b/server/src/services/Currencies/CurrenciesService.ts new file mode 100644 index 000000000..67d7feadd --- /dev/null +++ b/server/src/services/Currencies/CurrenciesService.ts @@ -0,0 +1,152 @@ +import { Inject, Container, Service } from 'typedi'; +import { + ICurrencyEditDTO, + ICurrencyDTO, + ICurrenciesService, + ICurrency +} from 'interfaces'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import { ServiceError } from 'exceptions'; +import TenancyService from 'services/Tenancy/TenancyService'; + +const ERRORS = { + CURRENCY_NOT_FOUND: 'currency_not_found', + CURRENCY_CODE_EXISTS: 'currency_code_exists' +}; + +@Service() +export default class CurrenciesService implements ICurrenciesService { + @Inject('logger') + logger: any; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject() + tenancy: TenancyService; + + /** + * Retrieve currency by given currency code or throw not found error. + * @param {number} tenantId + * @param {string} currencyCode + * @param {number} currencyId + */ + private async validateCurrencyCodeUniquiness(tenantId: number, currencyCode: string, currencyId?: number) { + const { Currency } = this.tenancy.models(tenantId); + + this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode }); + const foundCurrency = await Currency.query().onBuild((query) => { + query.findOne('currency_code', currencyCode); + + if (currencyId) { + query.whereNot('id', currencyId) + } + }); + if (foundCurrency) { + this.logger.info('[currencies] the currency code already exists.', { tenantId, currencyCode }); + throw new ServiceError(ERRORS.CURRENCY_CODE_EXISTS); + } + } + + /** + * Retrieve currency by the given currency code or throw service error. + * @param {number} tenantId + * @param {string} currencyCode + */ + private async getCurrencyByCodeOrThrowError(tenantId: number, currencyCode: string) { + const { Currency } = this.tenancy.models(tenantId); + + this.logger.info('[currencies] trying to validate currency code existance.', { tenantId, currencyCode }); + const foundCurrency = await Currency.query().findOne('currency_code', currencyCode); + + if (!foundCurrency) { + this.logger.info('[currencies] the given currency code not exists.', { tenantId, currencyCode }); + throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND); + } + return foundCurrency; + } + + /** + * Retrieve currency by given id or throw not found error. + * @param {number} tenantId + * @param {number} currencyId + */ + private async getCurrencyIdOrThrowError(tenantId: number, currencyId: number) { + const { Currency } = this.tenancy.models(tenantId); + + this.logger.info('[currencies] trying to validate currency by id existance.', { tenantId, currencyId }); + const foundCurrency = await Currency.query().findOne('id', currencyId); + + if (!foundCurrency) { + this.logger.info('[currencies] the currency code not found.', { tenantId, currencyId }); + throw new ServiceError(ERRORS.CURRENCY_NOT_FOUND); + } + return foundCurrency; + } + + /** + * Creates a new currency. + * @param {number} tenantId + * @param {ICurrencyDTO} currencyDTO + */ + public async newCurrency(tenantId: number, currencyDTO: ICurrencyDTO) { + const { Currency } = this.tenancy.models(tenantId); + this.logger.info('[currencies] try to insert new currency.', { tenantId, currencyDTO }); + + await this.validateCurrencyCodeUniquiness(tenantId, currencyDTO.currencyCode); + + await Currency.query().insert({ ...currencyDTO }); + this.logger.info('[currencies] the currency inserted successfully.', { tenantId, currencyDTO }); + } + + /** + * Edit details of the given currency. + * @param {number} tenantId + * @param {number} currencyId + * @param {ICurrencyDTO} currencyDTO + */ + public async editCurrency(tenantId: number, currencyId: number, currencyDTO: ICurrencyEditDTO): Promise { + const { Currency } = this.tenancy.models(tenantId); + + this.logger.info('[currencies] try to edit currency.', { tenantId, currencyId, currencyDTO }); + await this.getCurrencyIdOrThrowError(tenantId, currencyId); + + const currency = await Currency.query().patchAndFetchById(currencyId, { ...currencyDTO }); + this.logger.info('[currencies] the currency edited successfully.', { tenantId, currencyDTO }); + + return currency; + } + + /** + * Delete the given currency code. + * @param {number} tenantId + * @param {string} currencyCode + * @return {Promise<} + */ + public async deleteCurrency(tenantId: number, currencyCode: string): Promise { + const { Currency } = this.tenancy.models(tenantId); + this.logger.info('[currencies] trying to delete the given currency.', { tenantId, currencyCode }); + + await this.getCurrencyByCodeOrThrowError(tenantId, currencyCode); + + await Currency.query().where('currency_code', currencyCode).delete(); + this.logger.info('[currencies] the currency deleted successfully.', { tenantId, currencyCode }); + } + + /** + * Listing currencies. + * @param {number} tenantId + * @return {Promise} + */ + public async listCurrencies(tenantId: number): Promise { + const { Currency } = this.tenancy.models(tenantId); + + const currencies = await Currency.query().onBuild((query) => { + query.orderBy('createdAt', 'ASC'); + }); + return currencies; + } +} \ No newline at end of file diff --git a/server/src/services/ExchangeRates/ExchangeRatesService.ts b/server/src/services/ExchangeRates/ExchangeRatesService.ts new file mode 100644 index 000000000..2896157b1 --- /dev/null +++ b/server/src/services/ExchangeRates/ExchangeRatesService.ts @@ -0,0 +1,166 @@ +import moment from 'moment'; +import { difference } from 'lodash'; +import { Service, Inject } from 'typedi'; +import { ServiceError } from 'exceptions'; +import { + EventDispatcher, + EventDispatcherInterface, +} from 'decorators/eventDispatcher'; +import { + IExchangeRateDTO, + IExchangeRate, + IExchangeRatesService, + IExchangeRateEditDTO, + IExchangeRateFilter, +} from 'interfaces'; +import TenancyService from 'services/Tenancy/TenancyService'; + +const ERRORS = { + NOT_FOUND_EXCHANGE_RATES: 'NOT_FOUND_EXCHANGE_RATES', + EXCHANGE_RATE_PERIOD_EXISTS: 'EXCHANGE_RATE_PERIOD_EXISTS', + EXCHANGE_RATE_NOT_FOUND: 'EXCHANGE_RATE_NOT_FOUND', +}; + +@Service() +export default class ExchangeRatesService implements IExchangeRatesService { + @Inject('logger') + logger: any; + + @EventDispatcher() + eventDispatcher: EventDispatcherInterface; + + @Inject() + tenancy: TenancyService; + + /** + * Creates a new exchange rate. + * @param {number} tenantId + * @param {IExchangeRateDTO} exchangeRateDTO + * @returns {Promise} + */ + public async newExchangeRate(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + + this.logger.info('[exchange_rates] trying to insert new exchange rate.', { tenantId, exchangeRateDTO }); + await this.validateExchangeRatePeriodExistance(tenantId, exchangeRateDTO); + + const exchangeRate = await ExchangeRate.query().insertAndFetch({ + ...exchangeRateDTO, + date: moment(exchangeRateDTO.date).format('YYYY-MM-DD'), + }); + this.logger.info('[exchange_rates] inserted successfully.', { tenantId, exchangeRateDTO }); + return exchangeRate; + } + + /** + * Edits the exchange rate details. + * @param {number} tenantId - Tenant id. + * @param {number} exchangeRateId - Exchange rate id. + * @param {IExchangeRateEditDTO} editExRateDTO - Edit exchange rate DTO. + */ + public async editExchangeRate(tenantId: number, exchangeRateId: number, editExRateDTO: IExchangeRateEditDTO): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + + this.logger.info('[exchange_rates] trying to edit exchange rate.', { tenantId, exchangeRateId, editExRateDTO }); + await this.validateExchangeRateExistance(tenantId, exchangeRateId); + + await ExchangeRate.query().where('id', exchangeRateId).update({ ...editExRateDTO }); + this.logger.info('[exchange_rates] exchange rate edited successfully.', { tenantId, exchangeRateId, editExRateDTO }); + } + + /** + * Deletes the given exchange rate. + * @param {number} tenantId - Tenant id. + * @param {number} exchangeRateId - Exchange rate id. + */ + public async deleteExchangeRate(tenantId: number, exchangeRateId: number): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + await this.validateExchangeRateExistance(tenantId, exchangeRateId); + + await ExchangeRate.query().findById(exchangeRateId).delete(); + } + + /** + * Listing exchange rates details. + * @param {number} tenantId - Tenant id. + * @param {IExchangeRateFilter} exchangeRateFilter - Exchange rates list filter. + */ + public async listExchangeRates(tenantId: number, exchangeRateFilter: IExchangeRateFilter): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + const exchangeRates = await ExchangeRate.query() + .pagination(exchangeRateFilter.page - 1, exchangeRateFilter.pageSize); + + return exchangeRates; + } + + /** + * Deletes exchange rates in bulk. + * @param {number} tenantId + * @param {number[]} exchangeRatesIds + */ + public async deleteBulkExchangeRates(tenantId: number, exchangeRatesIds: number[]): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + + this.logger.info('[exchange_rates] trying delete in bulk.', { tenantId, exchangeRatesIds }); + await this.validateExchangeRatesIdsExistance(tenantId, exchangeRatesIds); + + await ExchangeRate.query().whereIn('id', exchangeRatesIds).delete(); + this.logger.info('[exchange_rates] deleted successfully.', { tenantId, exchangeRatesIds }); + } + + /** + * Validates period of the exchange rate existance. + * @param {number} tenantId - Tenant id. + * @param {IExchangeRateDTO} exchangeRateDTO - Exchange rate DTO. + * @return {Promise} + */ + private async validateExchangeRatePeriodExistance(tenantId: number, exchangeRateDTO: IExchangeRateDTO): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + + this.logger.info('[exchange_rates] trying to validate period existance.', { tenantId }); + const foundExchangeRate = await ExchangeRate.query() + .where('currency_code', exchangeRateDTO.currencyCode) + .where('date', exchangeRateDTO.date); + + if (foundExchangeRate.length > 0) { + this.logger.info('[exchange_rates] given exchange rate period exists.', { tenantId }); + throw new ServiceError(ERRORS.EXCHANGE_RATE_PERIOD_EXISTS); + } + } + + /** + * Validate the given echange rate id existance. + * @param {number} tenantId - Tenant id. + * @param {number} exchangeRateId - Exchange rate id. + * @returns {Promise} + */ + private async validateExchangeRateExistance(tenantId: number, exchangeRateId: number) { + const { ExchangeRate } = this.tenancy.models(tenantId); + + this.logger.info('[exchange_rates] trying to validate exchange rate id existance.', { tenantId, exchangeRateId }); + const foundExchangeRate = await ExchangeRate.query().findById(exchangeRateId); + + if (!foundExchangeRate) { + this.logger.info('[exchange_rates] exchange rate not found.', { tenantId, exchangeRateId }); + throw new ServiceError(ERRORS.EXCHANGE_RATE_NOT_FOUND); + } + } + + /** + * Validates exchange rates ids existance. + * @param {number} tenantId - Tenant id. + * @param {number[]} exchangeRatesIds - Exchange rates ids. + * @returns {Promise} + */ + private async validateExchangeRatesIdsExistance(tenantId: number, exchangeRatesIds: number[]): Promise { + const { ExchangeRate } = this.tenancy.models(tenantId); + + const storedExchangeRates = await ExchangeRate.query().whereIn('id', exchangeRatesIds); + const storedExchangeRatesIds = storedExchangeRates.map((category) => category.id); + const notFoundExRates = difference(exchangeRatesIds, storedExchangeRatesIds); + + if (notFoundExRates.length > 0) { + throw new ServiceError(ERRORS.NOT_FOUND_EXCHANGE_RATES); + } + } +} \ No newline at end of file diff --git a/server/src/services/Tenancy/TenantsManager.ts b/server/src/services/Tenancy/TenantsManager.ts index d83ab7638..fb38b47b5 100644 --- a/server/src/services/Tenancy/TenantsManager.ts +++ b/server/src/services/Tenancy/TenantsManager.ts @@ -138,7 +138,7 @@ export default class TenantsManagerService implements ITenantManager{ /** * Throws error if the tenant database is not built yut. - * @param tenant + * @param {ITenant} tenant */ private throwErrorIfTenantNotBuilt(tenant: ITenant) { if (!tenant.initializedAt) { diff --git a/server/src/subscribers/events.ts b/server/src/subscribers/events.ts index 0d5150c0a..975036766 100644 --- a/server/src/subscribers/events.ts +++ b/server/src/subscribers/events.ts @@ -1,6 +1,9 @@ export default { + /** + * Authentication service. + */ auth: { login: 'onLogin', register: 'onRegister', @@ -8,23 +11,35 @@ export default { resetPassword: 'onResetPassword', }, + /** + * Invite users service. + */ inviteUser: { acceptInvite: 'onUserAcceptInvite', sendInvite: 'onUserSendInvite', checkInvite: 'onUserCheckInvite' }, + /** + * Organization managment service. + */ organization: { build: 'onOrganizationBuild', seeded: 'onOrganizationSeeded', }, + /** + * Tenants managment service. + */ tenantManager: { databaseCreated: 'onDatabaseCreated', tenantMigrated: 'onTenantMigrated', tenantSeeded: 'onTenantSeeded', }, + /** + * Manual journals service. + */ manualJournals: { onCreated: 'onManualJournalCreated', onEdited: 'onManualJournalEdited', diff --git a/server/src/system/repositories/SubscriptionRepository.ts b/server/src/system/repositories/SubscriptionRepository.ts index 6be38c802..39d3a2d55 100644 --- a/server/src/system/repositories/SubscriptionRepository.ts +++ b/server/src/system/repositories/SubscriptionRepository.ts @@ -10,7 +10,7 @@ export default class SubscriptionRepository extends SystemRepository{ /** * Retrieve subscription from a given slug in specific tenant. * @param {string} slug - * @param {number] tenantId + * @param {number} tenantId */ getBySlugInTenant(slug: string, tenantId: number) { const key = `subscription.slug.${slug}.tenant.${tenantId}`;