diff --git a/server/src/database/factories/index.js b/server/src/database/factories/index.js index 2f0cc8998..8f9e73296 100644 --- a/server/src/database/factories/index.js +++ b/server/src/database/factories/index.js @@ -250,6 +250,14 @@ factory.define('currency', 'currencies', async () => { }; }); +factory.define('exchange_rate', 'exchange_rates', async () => { + return { + date: '2020-02-02', + currency_code: 'USD', + exchange_rate: faker.random.number(), + }; +}); + factory.define('budget', 'budgets', async () => { return { name: faker.lorem.slug(), diff --git a/server/src/database/migrations/20200419191832_create_exchange_rates_table.js b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js new file mode 100644 index 000000000..fdf4509d0 --- /dev/null +++ b/server/src/database/migrations/20200419191832_create_exchange_rates_table.js @@ -0,0 +1,13 @@ + +exports.up = function(knex) { + return knex.schema.createTable('exchange_rates', table => { + table.increments(); + table.string('currency_code', 4); + table.decimal('exchange_rate'); + table.date('date'); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('exchange_rates'); +}; diff --git a/server/src/http/controllers/ExchangeRates.js b/server/src/http/controllers/ExchangeRates.js new file mode 100644 index 000000000..5fddd2163 --- /dev/null +++ b/server/src/http/controllers/ExchangeRates.js @@ -0,0 +1,166 @@ +import express from 'express'; +import { check, param, query, validationResult } from 'express-validator'; +import moment from 'moment'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; +import ExchangeRate from '@/models/ExchangeRate'; + +export default { + /** + * Constructor method. + */ + router() { + const router = express.Router(); + router.use(jwtAuth); + + 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('/: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 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 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 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 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 }); + } + }, +} \ No newline at end of file diff --git a/server/src/http/index.js b/server/src/http/index.js index f8f706843..6ac3d22c0 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -21,6 +21,7 @@ import Suppliers from '@/http/controllers/Suppliers'; import Bills from '@/http/controllers/Bills'; import CurrencyAdjustment from './controllers/CurrencyAdjustment'; import Resources from './controllers/Resources'; +import ExchangeRates from '@/http/controllers/ExchangeRates'; // import SalesReports from '@/http/controllers/SalesReports'; // import PurchasesReports from '@/http/controllers/PurchasesReports'; @@ -47,6 +48,7 @@ export default (app) => { // app.use('/api/bills', Bills.router()); app.use('/api/budget', Budget.router()); app.use('/api/resources', Resources.router()); + app.use('/api/exchange_rates', ExchangeRates.router()); // app.use('/api/currency_adjustment', CurrencyAdjustment.router()); // app.use('/api/reports/sales', SalesReports.router()); // app.use('/api/reports/purchases', PurchasesReports.router()); diff --git a/server/src/models/ExchangeRate.js b/server/src/models/ExchangeRate.js new file mode 100644 index 000000000..57343da17 --- /dev/null +++ b/server/src/models/ExchangeRate.js @@ -0,0 +1,10 @@ +import BaseModel from '@/models/Model'; + +export default class ExchangeRate extends BaseModel { + /** + * Table name + */ + static get tableName() { + return 'exchange_rates'; + } +} diff --git a/server/tests/routes/exchange_rates.test.js b/server/tests/routes/exchange_rates.test.js new file mode 100644 index 000000000..043627ce1 --- /dev/null +++ b/server/tests/routes/exchange_rates.test.js @@ -0,0 +1,186 @@ +import moment from 'moment'; +import { + request, + create, + expect, + login, +} from '~/testInit'; +import ExchangeRate from '../../src/models/ExchangeRate'; + +let loginRes; + +describe.only('route: /exchange_rates/', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); + describe('POST: `/api/exchange_rates`', () => { + it('Should response unauthorized in case the user was not logged in.', async () => { + const res = await request() + .post('/api/exchange_rates') + .send(); + + expect(res.status).equals(401); + expect(res.body.message).equals('unauthorized'); + }); + + it('Should `currency_code` be required.', async () => { + const res = await request() + .post('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'currency_code', location: 'body', + }); + }); + + it('Should `exchange_rate` be required.', async () => { + const res = await request() + .post('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'exchange_rate', location: 'body', + }); + }); + + it('Should date be required', async () => { + const res = await request() + .post('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'date', location: 'body', + }); + }); + + it('Should response date and currency code is already exists.', async () => { + await create('exchange_rate', { + date: '2020-02-02', + currency_code: 'USD', + exchange_rate: 4.4, + }); + const res = await request() + .post('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send({ + date: '2020-02-02', + currency_code: 'USD', + exchange_rate: 4.4, + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.deep.equals({ + type: 'EXCHANGE.RATE.DATE.PERIOD.DEFINED', code: 200, + }); + }); + + it('Should save the given exchange rate to the storage.', async () => { + const res = await request() + .post('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send({ + date: '2020-02-02', + currency_code: 'USD', + exchange_rate: 4.4, + }); + expect(res.status).equals(200); + + const foundExchangeRate = await ExchangeRate.query() + .where('currency_code', 'USD'); + + expect(foundExchangeRate.length).equals(1); + expect( + moment(foundExchangeRate[0].date).format('YYYY-MM-DD'), + ).equals('2020-02-02'); + expect(foundExchangeRate[0].currencyCode).equals('USD'); + expect(foundExchangeRate[0].exchangeRate).equals(4.4); + }); + }); + + describe('GET: `/api/exchange_rates', () => { + it('Should retrieve all exchange rates with pagination meta.', async () => { + await create('exchange_rate'); + await create('exchange_rate'); + await create('exchange_rate'); + + const res = await request() + .get('/api/exchange_rates') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(200); + expect(res.body.exchange_rates.results.length).equals(3); + }); + }); + + describe('POST: `/api/exchange_rates/:id`', () => { + it('Should response the given exchange rate not found.', async () => { + const res = await request() + .post('/api/exchange_rates/100') + .set('x-access-token', loginRes.body.token) + .send({ + date: '2020-02-02', + currency_code: 'USD', + exchange_rate: 4.4, + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.deep.equals({ + type: 'EXCHANGE.RATE.NOT.FOUND', code: 200, + }); + }); + + it('Should update exchange rate of the given id on the storage.', async () => { + const exRate = await create('exchange_rate'); + const res = await request() + .post(`/api/exchange_rates/${exRate.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + exchange_rate: 4.4, + }); + expect(res.status).equals(200); + + const foundExchangeRate = await ExchangeRate.query() + .where('id', exRate.id); + + expect(foundExchangeRate.length).equals(1); + expect(foundExchangeRate[0].exchangeRate).equals(4.4); + }); + }); + + describe('DELETE: `/api/exchange_rates/:id`', () => { + it('Should response the given exchange rate id not found.', async () => { + const res = await request() + .delete('/api/exchange_rates/100') + .set('x-access-token', loginRes.body.token) + .send(); + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'EXCHANGE.RATE.NOT.FOUND', code: 200, + }); + }); + + it('Should delete the given exchange rate id from the storage.', async () => { + const exRate = await create('exchange_rate'); + const res = await request() + .delete(`/api/exchange_rates/${exRate.id}`) + .set('x-access-token', loginRes.body.token) + .send(); + + const foundRates = await ExchangeRate.query(); + expect(foundRates.length).equals(0); + }); + }); +}); diff --git a/server/tests/routes/itemsCategories.test.js b/server/tests/routes/itemsCategories.test.js index 8066eb1ca..8597b7a69 100644 --- a/server/tests/routes/itemsCategories.test.js +++ b/server/tests/routes/itemsCategories.test.js @@ -209,7 +209,7 @@ describe('routes: /item_categories/', () => { }); }); - describe.only('GET: `/item_categories`', () => { + describe('GET: `/item_categories`', () => { it('Should retrieve list of item categories.', async () => { const category1 = await create('item_category');