diff --git a/server/src/database/factories/index.js b/server/src/database/factories/index.js index 17503d50c..275a78020 100644 --- a/server/src/database/factories/index.js +++ b/server/src/database/factories/index.js @@ -181,6 +181,17 @@ factory.define('resource_field', 'resource_fields', async () => { }; }); +factory.define('resource_custom_field_metadata', 'resource_custom_fields_metadata', async () => { + const resource = await factory.create('resource'); + + return { + resource_id: resource.id, + resource_item_id: 1, + key: faker.lorem.words(), + value: faker.lorem.words(), + }; +}); + factory.define('view_role', 'view_roles', async () => { const view = await factory.create('view'); const field = await factory.create('resource_field'); diff --git a/server/src/database/migrations/20190822214303_create_items_table.js b/server/src/database/migrations/20190822214303_create_items_table.js index ff6b9dc5b..36491b4d4 100644 --- a/server/src/database/migrations/20190822214303_create_items_table.js +++ b/server/src/database/migrations/20190822214303_create_items_table.js @@ -3,7 +3,7 @@ exports.up = function (knex) { return knex.schema.createTable('items', (table) => { table.increments(); table.string('name'); - table.integer('type_id').unsigned(); + table.string('type'); table.decimal('cost_price').unsigned(); table.decimal('sell_price').unsigned(); table.string('currency_code', 3); diff --git a/server/src/database/migrations/20190822214905_create_resource_fields_table.js b/server/src/database/migrations/20190822214905_create_resource_fields_table.js index 1c2fec0b6..7518b1696 100644 --- a/server/src/database/migrations/20190822214905_create_resource_fields_table.js +++ b/server/src/database/migrations/20190822214905_create_resource_fields_table.js @@ -10,6 +10,7 @@ exports.up = function (knex) { table.boolean('active'); table.boolean('predefined'); table.boolean('columnable'); + table.integer('index'); table.json('options'); table.integer('resource_id').unsigned().references('id').inTable('resources'); }); diff --git a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js index 2c2c97331..389472121 100644 --- a/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js +++ b/server/src/database/migrations/20200104232647_create_accounts_transactions_table.js @@ -9,6 +9,7 @@ exports.up = function(knex) { table.integer('reference_id'); table.integer('account_id').unsigned().references('id').inTable('accounts'); table.string('note'); + table.boolean('draft').defaultTo(false); table.integer('user_id').unsigned().references('id').inTable('users'); table.date('date'); table.timestamps(); diff --git a/server/src/database/migrations/20200105014405_create_expenses_table.js b/server/src/database/migrations/20200105014405_create_expenses_table.js index f6384e807..4cb326302 100644 --- a/server/src/database/migrations/20200105014405_create_expenses_table.js +++ b/server/src/database/migrations/20200105014405_create_expenses_table.js @@ -9,6 +9,7 @@ exports.up = function(knex) { table.integer('expense_account_id').unsigned().references('id').inTable('accounts'); table.integer('payment_account_id').unsigned().references('id').inTable('accounts'); table.string('reference'); + table.boolean('published').defaultTo(false); table.integer('user_id').unsigned().references('id').inTable('users'); table.date('date'); // table.timestamps(); diff --git a/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js b/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js new file mode 100644 index 000000000..74c1c89c3 --- /dev/null +++ b/server/src/database/migrations/20200125173323_create_resource_custom_fields_metadata_table.js @@ -0,0 +1,14 @@ + +exports.up = function(knex) { + return knex.schema.createTable('resource_custom_fields_metadata', (table) => { + table.increments(); + table.integer('resource_id').unsigned().references('id').inTable('resources'); + table.integer('resource_item_id').unsigned(); + table.string('key'); + table.string('value'); + }) +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('resource_custom_fields_metadata'); +}; diff --git a/server/src/http/controllers/AccountOpeningBalance.js b/server/src/http/controllers/AccountOpeningBalance.js index 021f0d99e..ec3fb3c7f 100644 --- a/server/src/http/controllers/AccountOpeningBalance.js +++ b/server/src/http/controllers/AccountOpeningBalance.js @@ -1,10 +1,13 @@ import express from 'express'; import { check, validationResult, oneOf } from 'express-validator'; import { difference } from 'lodash'; +import moment from 'moment'; import asyncMiddleware from '../middleware/asyncMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; import Account from '@/models/Account'; import JournalPoster from '@/services/Accounting/JournalPoster'; import JournalEntry from '@/services/Accounting/JournalEntry'; +import ManualJournal from '@/models/ManualJournal'; export default { /** @@ -13,6 +16,8 @@ export default { router() { const router = express.Router(); + router.use(jwtAuth); + router.post('/', this.openingBalnace.validation, asyncMiddleware(this.openingBalnace.handler)); @@ -27,11 +32,14 @@ export default { */ openingBalnace: { validation: [ + check('date').optional(), + check('note').optional().trim().escape(), + check('balance_adjustment_account').exists().isNumeric().toInt(), check('accounts').isArray({ min: 1 }), check('accounts.*.id').exists().isInt(), oneOf([ - check('accounts.*.debit').isNumeric().toFloat(), - check('accounts.*.credit').isNumeric().toFloat(), + check('accounts.*.debit').exists().isNumeric().toFloat(), + check('accounts.*.credit').exists().isNumeric().toFloat(), ]), ], async handler(req, res) { @@ -44,13 +52,19 @@ export default { } const { accounts } = req.body; + const { user } = req; + const form = { ...req.body }; + const date = moment(form.date).format('YYYY-MM-DD'); + const accountsIds = accounts.map((account) => account.id); - const accountsCollection = await Account.query() - .select(['id']) - .whereIn('id', accountsIds); + const storedAccounts = await Account.query() + .select(['id']).whereIn('id', accountsIds) + .withGraphFetched('type'); + + const accountsCollection = new Map(storedAccounts.map(i => [i.id, i])); // Get the stored accounts Ids and difference with submit accounts. - const accountsStoredIds = accountsCollection.map((account) => account.id); + const accountsStoredIds = storedAccounts.map((account) => account.id); const notFoundAccountsIds = difference(accountsIds, accountsStoredIds); const errorReasons = []; @@ -58,36 +72,76 @@ export default { const ids = notFoundAccountsIds.map((a) => parseInt(a, 10)); errorReasons.push({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids }); } + if (form.balance_adjustment_account) { + const account = await Account.query().findById(form.balance_adjustment_account); + + if (!account) { + errorReasons.push({ type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300 }); + } + } if (errorReasons.length > 0) { return res.boom.badData(null, { errors: errorReasons }); } - const sharedJournalDetails = new JournalEntry({ - referenceType: 'OpeningBalance', - referenceId: 1, - }); - const journalEntries = new JournalPoster(sharedJournalDetails); + const journalEntries = new JournalPoster(); accounts.forEach((account) => { - const entry = new JournalEntry({ - account: account.id, - accountNormal: account.type.normal, - }); + const storedAccount = accountsCollection.get(account.id); + // Can't continue in case the stored account was not found. + if (!storedAccount) { return; } + + const entryModel = new JournalEntry({ + referenceType: 'OpeningBalance', + account: account.id, + accountNormal: storedAccount.type.normal, + userId: user.id, + }); if (account.credit) { - entry.credit = account.credit; - journalEntries.credit(entry); + entryModel.entry.credit = account.credit; + journalEntries.credit(entryModel); } else if (account.debit) { - entry.debit = account.debit; - journalEntries.debit(entry); + entryModel.entry.debit = account.debit; + journalEntries.debit(entryModel); } }); + // Calculates the credit and debit balance of stacked entries. + const trial = journalEntries.getTrialBalance(); + if (trial.credit !== trial.debit) { + const entryModel = new JournalEntry({ + referenceType: 'OpeningBalance', + account: form.balance_adjustment_account, + accountNormal: 'credit', + userId: user.id, + }); + + if (trial.credit > trial.debit) { + entryModel.entry.credit = Math.abs(trial.credit); + journalEntries.credit(entryModel); + + } else if (trial.credit < trial.debit) { + entryModel.entry.debit = Math.abs(trial.debit); + journalEntries.debit(entryModel); + } + } + const manualJournal = await ManualJournal.query().insert({ + amount: Math.max(trial.credit, trial.debit), + transaction_type: 'OpeningBalance', + date, + note: form.note, + user_id: user.id, + }); + + journalEntries.entries = journalEntries.entries.map((entry) => ({ + ...entry, + referenceId: manualJournal.id, + })); await Promise.all([ journalEntries.saveEntries(), journalEntries.saveBalance(), ]); - return res.status(200).send(); + return res.status(200).send({ id: manualJournal.id }); }, }, }; diff --git a/server/src/http/controllers/Budget.js b/server/src/http/controllers/Budget.js index b21a91f8c..b5db9d5d5 100644 --- a/server/src/http/controllers/Budget.js +++ b/server/src/http/controllers/Budget.js @@ -69,7 +69,6 @@ export default { errors: [{ type: 'budget.not.found', code: 100 }], }); } - const accountTypes = await AccountType.query().where('balance_sheet', true); const [budgetEntries, accounts] = await Promise.all([ @@ -211,6 +210,7 @@ export default { query('profit_loss').optional().isBoolean().toBoolean(), query('page').optional().isNumeric().toInt(), query('page_size').isNumeric().toInt(), + query('custom_view_id').optional().isNumeric().toInt(), ], async handler(req, res) { const validationErrors = validationResult(req); @@ -241,7 +241,7 @@ export default { }); return res.status(200).send({ items: budgets.items, - }) + }); }, }, }; diff --git a/server/src/http/controllers/Expenses.js b/server/src/http/controllers/Expenses.js index bac3306fe..195bf0db2 100644 --- a/server/src/http/controllers/Expenses.js +++ b/server/src/http/controllers/Expenses.js @@ -16,6 +16,7 @@ import JWTAuth from '@/http/middleware/jwtAuth'; import AccountTransaction from '@/models/AccountTransaction'; import View from '@/models/View'; import Resource from '../../models/Resource'; +import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository'; export default { /** @@ -29,6 +30,10 @@ export default { this.newExpense.validation, asyncMiddleware(this.newExpense.handler)); + router.post('/:id/publish', + this.publishExpense.validation, + asyncMiddleware(this.publishExpense.handler)); + router.delete('/:id', this.deleteExpense.validation, asyncMiddleware(this.deleteExpense.handler)); @@ -60,6 +65,10 @@ export default { check('amount').exists().isNumeric().toFloat(), check('currency_code').optional(), check('exchange_rate').optional().isNumeric().toFloat(), + check('publish').optional().isBoolean().toBoolean(), + check('custom_fields').optional().isArray({ min: 1 }), + check('custom_fields.*.key').exists().trim().escape(), + check('custom_fields.*.value').exists(), ], async handler(req, res) { const validationErrors = validationResult(req); @@ -71,6 +80,7 @@ export default { } const form = { date: new Date(), + published: false, ...req.body, }; // Convert the date to the general format. @@ -83,16 +93,23 @@ export default { if (!paymentAccount) { errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100 }); } - const expenseAccount = await Account.query() - .findById(form.expense_account_id).first(); + const expenseAccount = await Account.query().findById(form.expense_account_id).first(); if (!expenseAccount) { errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 }); } + const customFields = new ResourceCustomFieldRepository('Expense'); + await customFields.load(); + + customFields.fillCustomFields(form.custom_fields); + + if (customFields.validateExistCustomFields()) { + errorReasons.push({ type: 'CUSTOM.FIELDS.SLUGS.NOT.EXISTS', code: 400 }); + } if (errorReasons.length > 0) { return res.status(400).send({ errors: errorReasons }); } - const expenseTransaction = await Expense.query().insert({ ...form }); + const expenseTransaction = await Expense.query().insertAndFetch({ ...form }); const journalEntries = new JournalPoster(); const creditEntry = new JournalEntry({ @@ -102,6 +119,7 @@ export default { date: form.date, account: expenseAccount.id, accountNormal: 'debit', + draft: !form.published, }); const debitEntry = new JournalEntry({ debit: form.amount, @@ -110,11 +128,13 @@ export default { date: form.date, account: paymentAccount.id, accountNormal: 'debit', + draft: !form.published, }); journalEntries.credit(creditEntry); journalEntries.debit(debitEntry); await Promise.all([ + customFields.saveCustomFields(expenseTransaction.id), journalEntries.saveEntries(), journalEntries.saveBalance(), ]); @@ -221,6 +241,55 @@ export default { }, }, + /** + * Publish the given expense id. + */ + publishExpense: { + validation: [ + param('id').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 } = req.params; + const errorReasons = []; + const expense = await Expense.query().findById(id); + + if (!expense) { + errorReasons.push({ type: 'EXPENSE.NOT.FOUND', code: 100 }); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + + if (expense.published) { + errorReasons.push({ type: 'EXPENSE.ALREADY.PUBLISHED', code: 200 }); + } + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + + await AccountTransaction.query() + .where('reference_id', expense.id) + .where('reference_type', 'Expense') + .patch({ + draft: false, + }); + + await Expense.query() + .where('id', expense.id) + .update({ published: true }); + + return res.status(200).send(); + }, + }, + /** * Retrieve paginated expenses list. */ @@ -324,7 +393,7 @@ export default { expenseEntriesCollect.reverseEntries(); await Promise.all([ - expenseTransaction.delete(), + Expense.query().findById(expenseTransaction.id).delete(), expenseEntriesCollect.deleteEntries(), expenseEntriesCollect.saveBalance(), ]); diff --git a/server/src/http/controllers/Fields.js b/server/src/http/controllers/Fields.js index 603fe5876..0962a663e 100644 --- a/server/src/http/controllers/Fields.js +++ b/server/src/http/controllers/Fields.js @@ -16,7 +16,7 @@ export default { router() { const router = express.Router(); - router.post('/resource/:resource_id', + router.post('/resource/:resource_name', this.addNewField.validation, asyncMiddleware(this.addNewField.handler)); @@ -28,11 +28,11 @@ export default { this.changeStatus.validation, asyncMiddleware(this.changeStatus.handler)); - router.get('/:field_id', - asyncMiddleware(this.getField.handler)); + // router.get('/:field_id', + // asyncMiddleware(this.getField.handler)); - router.delete('/:field_id', - asyncMiddleware(this.deleteField.handler)); + // router.delete('/:field_id', + // asyncMiddleware(this.deleteField.handler)); return router; }, @@ -44,47 +44,44 @@ export default { */ addNewField: { validation: [ - param('resource_id').toInt(), + param('resource_name').exists().trim().escape(), check('label').exists().escape().trim(), check('data_type').exists().isIn(TYPES), check('help_text').optional(), check('default').optional(), check('options').optional().isArray(), + check('options.*.key').exists().isNumeric().toInt(), + check('options.*.value').exists(), ], async handler(req, res) { - const { resource_id: resourceId } = req.params; + const { resource_name: resourceName } = req.params; const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.boom.badData(null, { - code: 'VALIDATION_ERROR', ...validationErrors, + code: 'validation_error', ...validationErrors, }); } - const resource = await Resource.where('id', resourceId).fetch(); + const resource = await Resource.query().where('name', resourceName).first(); if (!resource) { return res.boom.notFound(null, { errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }], }); } + const form = { options: [], ...req.body }; + const choices = form.options.map((option) => ({ key: option.key, value: option.value })); - const { label, data_type: dataType, help_text: helpText } = req.body; - const { default: defaultValue, options } = req.body; - - const choices = options.map((option, index) => ({ key: index + 1, value: option })); - - const field = ResourceField.forge({ - data_type: dataType, - label_name: label, - help_text: helpText, - default: defaultValue, + const storedResource = await ResourceField.query().insertAndFetch({ + data_type: form.data_type, + label_name: form.label, + help_text: form.help_text, + default: form.default, resource_id: resource.id, options: choices, + index: -1, }); - - await field.save(); - - return res.status(200).send(); + return res.status(200).send({ id: storedResource.id }); }, }, @@ -93,12 +90,14 @@ export default { */ editField: { validation: [ - param('field_id').toInt(), + param('field_id').exists().isNumeric().toInt(), check('label').exists().escape().trim(), - check('data_type').exists(), + check('data_type').exists().isIn(TYPES), check('help_text').optional(), check('default').optional(), check('options').optional().isArray(), + check('options.*.key').exists().isNumeric().toInt(), + check('options.*.value').exists(), ], async handler(req, res) { const { field_id: fieldId } = req.params; @@ -106,52 +105,28 @@ export default { if (!validationErrors.isEmpty()) { return res.boom.badData(null, { - code: 'VALIDATION_ERROR', ...validationErrors, + code: 'validation_error', ...validationErrors, }); } - - const field = await ResourceField.where('id', fieldId).fetch(); + const field = await ResourceField.query().findById(fieldId); if (!field) { return res.boom.notFound(null, { errors: [{ type: 'FIELD_NOT_FOUND', code: 100 }], }); } - // Sets the default value of optional fields. const form = { options: [], ...req.body }; + const choices = form.options.map((option) => ({ key: option.key, value: option.value })); - const { label, data_type: dataType, help_text: helpText } = form; - const { default: defaultValue, options } = form; - - const storedFieldOptions = field.attributes.options || []; - let lastChoiceIndex = 0; - storedFieldOptions.forEach((option) => { - const key = parseInt(option.key, 10); - if (key > lastChoiceIndex) { - lastChoiceIndex = key; - } - }); - const savedOptionKeys = options.filter((op) => typeof op === 'object'); - const notSavedOptionsKeys = options.filter((op) => typeof op !== 'object'); - - const choices = [ - ...savedOptionKeys, - ...notSavedOptionsKeys.map((option) => { - lastChoiceIndex += 1; - return { key: lastChoiceIndex, value: option }; - }), - ]; - - await field.save({ - data_type: dataType, - label_name: label, - help_text: helpText, - default: defaultValue, + await ResourceField.query().findById(field.id).update({ + data_type: form.data_type, + label_name: form.label, + help_text: form.help_text, + default: form.default, options: choices, }); - - return res.status(200).send({ id: field.get('id') }); + return res.status(200).send({ id: field.id }); }, }, @@ -162,11 +137,25 @@ export default { */ fieldsList: { validation: [ - param('resource_id').toInt(), + param('resource_name').toInt(), ], async handler(req, res) { - const { resource_id: resourceId } = req.params; - const fields = await ResourceField.where('resource_id', resourceId).fetchAll(); + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const { resource_name: resourceName } = req.params; + const resource = await Resource.query().where('name', resourceName).first(); + + if (!resource) { + return res.boom.notFound(null, { + errors: [{ type: 'RESOURCE_NOT_FOUND', code: 100 }], + }); + } + const fields = await ResourceField.where('resource_id', resource.id).fetchAll(); return res.status(200).send({ fields: fields.toJSON() }); }, @@ -182,7 +171,7 @@ export default { ], async handler(req, res) { const { field_id: fieldId } = req.params; - const field = await ResourceField.where('id', fieldId).fetch(); + const field = await ResourceField.query().findById(fieldId); if (!field) { return res.boom.notFound(null, { @@ -191,9 +180,9 @@ export default { } const { active } = req.body; - await field.save({ active }); + await ResourceField.query().findById(field.id).patch({ active }); - return res.status(200).send({ id: field.get('id') }); + return res.status(200).send({ id: field.id }); }, }, diff --git a/server/src/http/controllers/FinancialStatements.js b/server/src/http/controllers/FinancialStatements.js index e7584215f..d166ba9e8 100644 --- a/server/src/http/controllers/FinancialStatements.js +++ b/server/src/http/controllers/FinancialStatements.js @@ -50,13 +50,9 @@ export default { this.profitLossSheet.validation, asyncMiddleware(this.profitLossSheet.handler)); - // router.get('/cash_flow_statement', - // this.cashFlowStatement.validation, - // asyncMiddleware(this.cashFlowStatement.handler)); - - // router.get('/badget_verses_actual', - // this.badgetVersesActuals.validation, - // asyncMiddleware(this.badgetVersesActuals.handler)); + router.get('/cash_flow_statement', + this.cashFlowStatement.validation, + asyncMiddleware(this.cashFlowStatement.handler)); return router; }, @@ -510,17 +506,12 @@ export default { ], async handler(req, res) { - return res.status(200).send(); - }, - }, - - - badgetVersesActuals: { - validation: [ - - ], - async handler(req, res) { - + return res.status(200).send({ + meta: {}, + operating: [], + financing: [], + investing: [], + }); }, }, } diff --git a/server/src/http/controllers/Items.js b/server/src/http/controllers/Items.js index 49627b5f1..9a2e988a3 100644 --- a/server/src/http/controllers/Items.js +++ b/server/src/http/controllers/Items.js @@ -19,18 +19,18 @@ export default { router.use(jwtAuth); - // router.post('/:id', - // this.editItem.validation, - // asyncMiddleware(this.editCategory.handler)); + router.post('/:id', + this.editItem.validation, + asyncMiddleware(this.editItem.handler)); router.post('/', - permit('create'), + // permit('create'), this.newItem.validation, asyncMiddleware(this.newItem.handler)); - // router.delete('/:id', - // this.deleteItem.validation, - // asyncMiddleware(this.deleteItem.handler)); + router.delete('/:id', + this.deleteItem.validation, + asyncMiddleware(this.deleteItem.handler)); // router.get('/:id', // this.getCategory.validation, @@ -49,14 +49,14 @@ export default { newItem: { validation: [ check('name').exists(), - check('type_id').exists().isInt(), - check('buy_price').exists().isNumeric(), + check('type').exists().trim().escape().isIn(['service', 'product']), check('cost_price').exists().isNumeric(), + check('sell_price').exists().isNumeric(), check('cost_account_id').exists().isInt(), check('sell_account_id').exists().isInt(), check('category_id').optional().isInt(), - check('custom_fields').isArray({ min: 1 }), + check('custom_fields').optional().isArray({ min: 1 }), check('custom_fields.*.key').exists().isNumeric().toInt(), check('custom_fields.*.value').exists(), @@ -70,13 +70,16 @@ export default { code: 'validation_error', ...validationErrors, }); } - const form = { ...req.body }; + const form = { + custom_fields: [], + ...req.body, + }; const errorReasons = []; - const costAccountPromise = Account.where('id', form.cost_account_id).fetch(); - const sellAccountPromise = Account.where('id', form.sell_account_id).fetch(); + const costAccountPromise = Account.query().findById(form.cost_account_id); + const sellAccountPromise = Account.query().findById(form.sell_account_id); const itemCategoryPromise = (form.category_id) - ? ItemCategory.where('id', form.category_id).fetch() : null; + ? ItemCategory.query().findById(form.category_id) : null; // Validate the custom fields key and value type. if (form.custom_fields.length > 0) { @@ -98,6 +101,76 @@ export default { errorReasons.push({ type: 'FIELD_KEY_NOT_FOUND', code: 150, fields: notFoundFields }); } } + const [costAccount, sellAccount, itemCategory] = await Promise.all([ + costAccountPromise, sellAccountPromise, itemCategoryPromise, + ]); + if (!costAccount) { + errorReasons.push({ type: 'COST_ACCOUNT_NOT_FOUND', code: 100 }); + } + if (!sellAccount) { + errorReasons.push({ type: 'SELL_ACCOUNT_NOT_FOUND', code: 120 }); + } + if (!itemCategory && form.category_id) { + errorReasons.push({ type: 'ITEM_CATEGORY_NOT_FOUND', code: 140 }); + } + if (errorReasons.length > 0) { + return res.boom.badRequest(null, { errors: errorReasons }); + } + const item = await Item.query().insertAndFetch({ + name: form.name, + type: form.type, + cost_price: form.cost_price, + sell_price: form.sell_price, + sell_account_id: form.sell_account_id, + cost_account_id: form.cost_account_id, + currency_code: form.currency_code, + note: form.note, + }); + return res.status(200).send({ id: item.id }); + }, + }, + + /** + * Edit the given item. + */ + editItem: { + validation: [ + check('name').exists(), + check('type').exists().trim().escape().isIn(['product', 'service']), + check('cost_price').exists().isNumeric(), + check('sell_price').exists().isNumeric(), + check('cost_account_id').exists().isInt(), + check('sell_account_id').exists().isInt(), + check('category_id').optional().isInt(), + check('note').optional(), + ], + 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 = { + custom_fields: [], + ...req.body, + }; + const item = await Item.query().findById(id); + + if (!item) { + return res.boom.notFound(null, { errors: [ + { type: 'ITEM.NOT.FOUND', code: 100 }, + ]}); + } + const errorReasons = []; + + const costAccountPromise = Account.query().findById(form.cost_account_id); + const sellAccountPromise = Account.query().findById(form.sell_account_id); + const itemCategoryPromise = (form.category_id) + ? ItemCategory.query().findById(form.category_id) : null; const [costAccount, sellAccount, itemCategory] = await Promise.all([ costAccountPromise, sellAccountPromise, itemCategoryPromise, @@ -114,41 +187,19 @@ export default { if (errorReasons.length > 0) { return res.boom.badRequest(null, { errors: errorReasons }); } - const item = Item.forge({ - name: req.body.name, - type_id: 1, - buy_price: req.body.buy_price, - sell_price: req.body.sell_price, - currency_code: req.body.currency_code, - note: req.body.note, + + const updatedItem = await Item.query().findById(id).patch({ + name: form.name, + type: form.type, + cost_price: form.cost_price, + sell_price: form.sell_price, + currency_code: form.currency_code, + sell_account_id: form.sell_account_id, + cost_account_id: form.cost_account_id, + category_id: form.category_id, + note: form.note, }); - await item.save(); - - return res.status(200).send(); - }, - }, - - /** - * Edit the given item. - */ - editItem: { - validation: [], - async handler(req, res) { - const { id } = req.params; - const validationErrors = validationResult(req); - - if (!validationErrors.isEmpty()) { - return res.boom.badData(null, { - code: 'validation_error', ...validationErrors, - }); - } - - const item = await Item.where('id', id).fetch(); - - if (!item) { - return res.boom.notFound(); - } - return res.status(200).send(); + return res.status(200).send({ id: updatedItem.id }); }, }, @@ -159,7 +210,7 @@ export default { validation: [], async handler(req, res) { const { id } = req.params; - const item = await Item.where('id', id).fetch(); + const item = await Item.query().findById(id); if (!item) { return res.boom.notFound(null, { @@ -167,7 +218,9 @@ export default { }); } - await item.destroy(); + // Delete the fucking the given item id. + await Item.query().findById(item.id).delete(); + return res.status(200).send(); }, }, diff --git a/server/src/http/index.js b/server/src/http/index.js index afe40d49d..27436f2cc 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -28,7 +28,7 @@ export default (app) => { app.use('/api/roles', Roles.router()); app.use('/api/accounts', Accounts.router()); app.use('/api/accounting', Accounting.router()); - app.use('/api/accounts_opeing_balance', AccountOpeningBalance.router()); + app.use('/api/accounts_opening_balances', AccountOpeningBalance.router()); app.use('/api/views', Views.router()); app.use('/api/fields', CustomFields.router()); app.use('/api/items', Items.router()); diff --git a/server/src/models/ManualJournal.js b/server/src/models/ManualJournal.js new file mode 100644 index 000000000..5361e18d2 --- /dev/null +++ b/server/src/models/ManualJournal.js @@ -0,0 +1,10 @@ +import BaseModel from '@/models/Model'; + +export default class ManualJournal extends BaseModel { + /** + * Table name. + */ + static get tableName() { + return 'manual_journals'; + } +} diff --git a/server/src/models/Model.js b/server/src/models/Model.js index 2367defb0..e39786ecb 100644 --- a/server/src/models/Model.js +++ b/server/src/models/Model.js @@ -8,7 +8,10 @@ export default class ModelBase extends Model { static query(...args) { return super.query(...args).runAfter((result) => { - return this.collection.from(result); + if (Array.isArray(result)) { + return this.collection.from(result); + } + return result; }); } } diff --git a/server/src/models/ResourceField.js b/server/src/models/ResourceField.js index b1e0d998f..5d470b1f6 100644 --- a/server/src/models/ResourceField.js +++ b/server/src/models/ResourceField.js @@ -11,6 +11,21 @@ export default class ResourceField extends BaseModel { return 'resource_fields'; } + static get jsonAttributes() { + return ['options']; + } + + /** + * Model modifiers. + */ + static get modifiers() { + return { + whereNotPredefined(query) { + query.whereNot('predefined', true); + }, + }; + } + /** * Timestamp columns. */ diff --git a/server/src/models/ResourceFieldMetadata.js b/server/src/models/ResourceFieldMetadata.js new file mode 100644 index 000000000..640b87816 --- /dev/null +++ b/server/src/models/ResourceFieldMetadata.js @@ -0,0 +1,39 @@ +import { Model } from 'objection'; +import path from 'path'; +import BaseModel from '@/models/Model'; +import MetableCollection from '@/lib/Metable/MetableCollection'; + +export default class ResourceFieldMetadata extends BaseModel { + /** + * Table name. + */ + static get tableName() { + return 'resource_custom_fields_metadata'; + } + + /** + * Override the resource field metadata collection. + */ + static get collection() { + return MetableCollection; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + return { + /** + * Resource field may belongs to resource model. + */ + resource: { + relation: Model.BelongsToOneRelation, + modelBase: path.join(__dirname, 'Resource'), + join: { + from: 'resource_fields.resource_id', + to: 'resources.id', + }, + }, + }; + } +} diff --git a/server/src/services/CustomFields/ResourceCustomFieldRepository.js b/server/src/services/CustomFields/ResourceCustomFieldRepository.js new file mode 100644 index 000000000..6ee00c792 --- /dev/null +++ b/server/src/services/CustomFields/ResourceCustomFieldRepository.js @@ -0,0 +1,123 @@ +import Resource from '@/models/Resource'; +import ResourceField from '@/models/ResourceField'; +import ResourceFieldMetadata from '@/models/ResourceFieldMetadata'; +import ModelBase from '@/models/Model'; + +export default class ResourceCustomFieldRepository { + + constructor(model) { + if (typeof model === 'function') { + this.resourceName = model.name; + } else if (typeof model === 'string') { + this.resourceName = model; + } + + // Custom fields of the given resource. + this.customFields = []; + this.filledCustomFields = []; + + // metadata of custom fields of the given resource. + this.metadata = {}; + this.resource = {}; + } + + /** + * Fetches metadata of custom fields of the given resource. + * @param {Integer} id - Resource item id. + */ + async fetchCustomFieldsMetadata(id) { + if (typeof id === 'undefined') { + throw new Error('Please define the resource item id.'); + } + if (!this.resource) { + throw new Error('Target resource model is not found.'); + } + const metadata = await ResourceFieldMetadata.query() + .where('resource_id', this.resource.id) + .where('resource_item_id', id); + + this.metadata[id] = metadata; + } + + /** + * Load resource. + */ + async loadResource() { + const resource = await Resource.query().where('name', this.resourceName).first(); + + if (!resource) { + throw new Error('There is no stored resource in the storage with the given model name.'); + } + this.setResource(resource); + } + + /** + * Load metadata of the resource. + */ + async loadResourceCustomFields() { + if (typeof this.resource.id === 'undefined') { + throw new Error('Please fetch resource details before fetch custom fields of the resource.'); + } + const customFields = await ResourceField.query() + .where('resource_id', this.resource.id) + .modify('whereNotPredefined'); + + this.setResourceCustomFields(customFields); + } + + /** + * Sets resource model. + * @param {Resource} resource - + */ + setResource(resource) { + this.resource = resource; + } + + /** + * Sets resource custom fields collection. + * @param {Array} customFields - + */ + setResourceCustomFields(customFields) { + this.customFields = customFields; + } + + /** + * Retrieve metadata of the resource custom fields. + * @param {Integer} itemId - + */ + getMetadata(itemId) { + return this.metadata[itemId] || this.metadata; + } + + fillCustomFields(id, attributes) { + if (typeof this.filledCustomFields[id] === 'undefined') { + this.filledCustomFields[id] = []; + } + attributes.forEach((attr) => { + this.filledCustomFields[id].push(attr); + this.fieldsMetadata[id].setMeta(attr.key, attr.value); + }); + } + + saveCustomFields(id) { + this.fieldsMetadata.saveMeta(); + } + + /** + * Validates the exist custom fields. + */ + validateExistCustomFields() { + + } + + toArray() { + return this.fieldsMetadata.toArray(); + } + + async load() { + await Promise.all([ + this.loadResource(), + this.loadResourceCustomFields(), + ]); + } +} \ No newline at end of file diff --git a/server/src/services/Moment/index.js b/server/src/services/Moment/index.js index 8c13303f4..525adfada 100644 --- a/server/src/services/Moment/index.js +++ b/server/src/services/Moment/index.js @@ -3,4 +3,4 @@ import { extendMoment } from 'moment-range'; const moment = extendMoment(Moment); -export default moment; \ No newline at end of file +export default moment; diff --git a/server/tests/lib/ResourceCustomFieldRepository.test.js b/server/tests/lib/ResourceCustomFieldRepository.test.js new file mode 100644 index 000000000..d8dcf2d20 --- /dev/null +++ b/server/tests/lib/ResourceCustomFieldRepository.test.js @@ -0,0 +1,97 @@ +import { + request, + expect, + create, + login, +} from '~/testInit'; +import Expense from '@/models/Expense'; +import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository'; +import ResourceFieldMetadata from '@/models/ResourceFieldMetadata'; + +let loginRes; + +describe('ResourceCustomFieldRepository', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); + + describe('constructor()', () => { + it('Should take the resource name from model class name', () => { + const customFieldsRepo = new ResourceCustomFieldRepository(Expense); + + expect(customFieldsRepo.resourceName).equals('Expense'); + }); + }); + + describe('fetchCustomFields', () => { + it('Should fetches all custom fields that associated with the resource.', async () => { + const resource = await create('resource', { name: 'Expense' }); + const resourceField = await create('resource_field', { resource_id: resource.id }); + const resourceField2 = await create('resource_field', { resource_id: resource.id }); + + const customFieldsRepo = new ResourceCustomFieldRepository(Expense); + await customFieldsRepo.fetchCustomFields(); + + expect(customFieldsRepo.customFields.length).equals(2); + }); + }); + + describe.only('fetchCustomFieldsMetadata', () => { + it('Should fetches all custom fields metadata that associated to the resource and resource item.', async () => { + const resource = await create('resource', { name: 'Expense' }); + const resourceField = await create('resource_field', { resource_id: resource.id }); + const resourceField2 = await create('resource_field', { resource_id: resource.id }); + + const expense = await create('expense'); + + const fieldMetadata = await create('resource_custom_field_metadata', { + resource_id: resource.id, resource_item_id: expense.id, + }); + const customFieldsRepo = new ResourceCustomFieldRepository(Expense); + + await customFieldsRepo.fetchCustomFields(); + await customFieldsRepo.fetchCustomFieldsMetadata(expense.id); + + expect(customFieldsRepo.metadata[expense.id].metadata.length).equals(1); + }); + }); + + describe('fillCustomFields', () => { + + }); + + describe('saveCustomFields', () => { + it('Should save the given custom fields metadata to the resource item.', () => { + const resource = await create('resource'); + const resourceField = await create('resource_field', { resource_field: resource.id }); + + const expense = await create('expense'); + const fieldMetadata = await create('resource_custom_field_metadata', { + resource_id: resource.id, resource_item_id: expense.id, + }); + + const customFieldsRepo = new ResourceCustomFieldRepository(Expense); + await customFieldsRepo.load(); + + customFieldsRepo.fillCustomFields(expense.id, [ + { key: resourceField.slug, value: 'Hello World' }, + ]); + + await customFieldsRepo.saveCustomFields(); + + const updateResourceFieldData = await ResourceFieldMetadata.query() + .where('resource_id', resource.id) + .where('resource_item_id', expense.id) + .first(); + + expect(updateResourceFieldData.value).equals('Hello World'); + }); + }); + + // describe('validateExistCustomFields', () => { + + // }); +}); \ No newline at end of file diff --git a/server/tests/routes/accountOpeningBalance.test.js b/server/tests/routes/accountOpeningBalance.test.js index 2dddea0d2..29dd3c547 100644 --- a/server/tests/routes/accountOpeningBalance.test.js +++ b/server/tests/routes/accountOpeningBalance.test.js @@ -1,55 +1,152 @@ -import { request, expect, create } from '~/testInit'; +import { request, expect, create, login } from '~/testInit'; +import ManualJournal from '../../src/models/ManualJournal'; +import AccountTransaction from '@/models/AccountTransaction'; +let loginRes; describe('routes: `/accountOpeningBalance`', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); describe('POST `/accountOpeningBalance`', () => { it('Should `accounts` be array type.', async () => { - const res = await request().post('/api/accountOpeningBalance').send({ - accounts: 1000, - }); + const res = await request() + .post('/api/accounts_opening_balances') + .send({ + accounts: 1000, + }); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); + expect(res.body.code).equals('validation_error'); }); it('Should `accounts.*.id` be integer', async () => { - const res = await request().post('/api/accountOpeningBalance').send({ - accounts: 1000, - }); - + const res = await request() + .post('/api/accounts_opening_balances') + .send({ + accounts: [ + { id: 'id' }, + ] + }); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.that.deep.equals({ + value: 'id', + msg: 'Invalid value', + param: 'accounts[0].id', + location: 'body', + }); }); it('Should `accounts.*.debit` be numeric.', async () => { - const res = await request().post('/api/accountOpeningBalance').send({ - accounts: [{ id: 'id' }], - }); - + const res = await request() + .post('/api/accounts_opening_balances') + .send({ + balance_adjustment_account: 10, + accounts: [{ id: 100, debit: 'id' }], + }); expect(res.status).equals(422); }); it('Should `accounts.*.id` be exist in the storage.', async () => { - const res = await request().post('/api/accountOpeningBalance').send({ - accounts: [ - { id: 100, credit: 100, debit: 100 }, - ], - }); - + const res = await request() + .post('/api/accounts_opening_balances') + .send({ + balance_adjustment_account: 10, + accounts: [ + { id: 100, credit: 100 }, + ], + }); expect(res.status).equals(422); expect(res.body.errors).include.something.that.deep.equals({ type: 'NOT_FOUND_ACCOUNT', code: 100, ids: [100], }); }); - it('Should store the given credit and debit to the account balance in the storage.', async () => { - const account = await create('account'); - const res = await request().post('/api/accountOpeningBalance').send({ - accounts: [ - { id: account.id, credit: 100, debit: 2 }, - ], - }); + it('Should response bad request in case balance adjustment account was not exist.', async () => { + const debitAccount = await create('account'); + const creditAccount = await create('account'); - console.log(res.status); + const res = await request() + .post('/api/accounts_opening_balances') + .send({ + balance_adjustment_account: 10, + accounts: [ + { id: debitAccount.id, credit: 100, debit: 2 }, + { id: creditAccount.id, credit: 0, debit: 100 }, + ] + }); + + expect(res.body.errors).include.something.that.deep.equals({ + type: 'BALANCE.ADJUSTMENT.ACCOUNT.NOT.EXIST', code: 300, + }); + }); + + it('Should store the manual transaction to the storage.', async () => { + const debitAccount = await create('account'); + const creditAccount = await create('account'); + const balance = await create('account'); + + const res = await request() + .post('/api/accounts_opening_balances') + .set('x-access-token', loginRes.body.token) + .send({ + balance_adjustment_account: balance.id, + accounts: [ + { id: debitAccount.id, credit: 100, debit: 2 }, + { id: creditAccount.id, credit: 0, debit: 100 }, + ] + }); + + const manualJournal = await ManualJournal.query().findById(res.body.id); + expect(manualJournal.amount).equals(100); + expect(manualJournal.transaction_type).equals('OpeningBalance'); + }); + + it('Should store the jouranl entries of account balance transaction.', async () => { + const debitAccount = await create('account'); + const creditAccount = await create('account'); + const balance = await create('account'); + + const res = await request() + .post('/api/accounts_opening_balances') + .set('x-access-token', loginRes.body.token) + .send({ + balance_adjustment_account: balance.id, + accounts: [ + { id: debitAccount.id, credit: 100, debit: 2 }, + { id: creditAccount.id, credit: 0, debit: 100 }, + ] + }); + + const transactions = await AccountTransaction.query() + .where('reference_type', 'OpeningBalance') + .where('reference_id', res.body.id); + + expect(transactions.length).equals(2); + }); + + it('Should adjustment with balance adjustment account with bigger than zero.', async () => { + const debitAccount = await create('account'); + const balance = await create('account'); + + const res = await request() + .post('/api/accounts_opening_balances') + .set('x-access-token', loginRes.body.token) + .send({ + balance_adjustment_account: balance.id, + accounts: [ + { id: debitAccount.id, credit: 0, debit: 100 }, + ] + }); + + const transactions = await AccountTransaction.query() + .where('reference_type', 'OpeningBalance') + .where('reference_id', res.body.id); + + expect(transactions.length).equals(2); }); }); }); diff --git a/server/tests/routes/budget.test.js b/server/tests/routes/budget.test.js index 4b35f70bc..19b3b5314 100644 --- a/server/tests/routes/budget.test.js +++ b/server/tests/routes/budget.test.js @@ -247,7 +247,7 @@ describe('routes: `/budget`', () => { }); }); - describe.only('GET: `/budget`', () => { + describe('GET: `/budget`', () => { it('Should retrieve all budgets with pagination metadata.', async () => { const res = await request() .get('/api/budget') diff --git a/server/tests/routes/budget_reports.test.js b/server/tests/routes/budget_reports.test.js index 63a0a23f4..aac4d0dd8 100644 --- a/server/tests/routes/budget_reports.test.js +++ b/server/tests/routes/budget_reports.test.js @@ -14,7 +14,7 @@ describe('routes: `/budget_reports`', () => { loginRes = null; }); - describe.only('GET: `/budget_verses_actual/:reportId`', () => { + describe('GET: `/budget_verses_actual/:reportId`', () => { it('Should retrieve columns of budget year range with quarter period.', async () => { const budget = await create('budget', { period: 'quarter' }); const budgetEntry = await create('budget_entry', { budget_id: budget.id }); diff --git a/server/tests/routes/expenses.test.js b/server/tests/routes/expenses.test.js index b41be9cca..114853609 100644 --- a/server/tests/routes/expenses.test.js +++ b/server/tests/routes/expenses.test.js @@ -1,4 +1,9 @@ -import { request, expect, create, login } from '~/testInit'; +import { + request, + expect, + create, + login, +} from '~/testInit'; import AccountTransaction from '@/models/AccountTransaction'; import Expense from '@/models/Expense'; @@ -78,7 +83,7 @@ describe('routes: /expenses/', () => { payment_account_id: 100, amount: 100, }); - + expect(res.body.errors).include.something.that.deep.equals({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200, }); @@ -92,14 +97,14 @@ describe('routes: /expenses/', () => { payment_account_id: 100, amount: 100, }); - + expect(res.body.errors).include.something.that.deep.equals({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100, }); }); it('Should response success with valid required data.', async () => { - const res = await request().post('/api/expenses') + const res = await request().post('/api/expenses') .set('x-access-token', loginRes.body.token) .send({ expense_account_id: expenseAccount.id, @@ -111,7 +116,8 @@ describe('routes: /expenses/', () => { }); it('Should record journal entries of expense transaction.', async () => { - const res = await request().post('/api/expenses') + const res = await request() + .post('/api/expenses') .set('x-access-token', loginRes.body.token) .send({ expense_account_id: expenseAccount.id, @@ -122,37 +128,63 @@ describe('routes: /expenses/', () => { const expensesEntries = await AccountTransaction.query() .where('reference_type', 'Expense') .where('reference_id', res.body.id); - + expect(expensesEntries.length).equals(2); }); - it('Should save expense transaction to the storage.', () => { - + it('Should save expense transaction to the storage.', async () => { + const res = await request() + .post('/api/expenses') + .set('x-access-token', loginRes.body.token) + .send({ + expense_account_id: expenseAccount.id, + payment_account_id: cashAccount.id, + amount: 100, + }); + + const expenseTransaction = await Expense.query().where('id', res.body.id); + + expect(expenseTransaction.amount).equals(100); + }); + + it('Should response bad request in case custom field slug was not exists in the storage.', () => { + + }); + + it('Should save expense custom fields to the storage.', () => { + }); }); describe('POST: `/expenses/:id`', () => { it('Should response unauthorized in case user was not authorized.', () => { + }); + it('Should response not found in case expense was not exist.', () => { + + }); + + it('Should update the expense transaction.', () => { + }); }); describe('DELETE: `/expenses/:id`', () => { it('Should response not found in case expense not found.', async () => { const res = await request() - .delete('/api/expense/1000') + .delete('/api/expenses/1000') .set('x-access-token', loginRes.body.token) .send(); - expect(res.body.reasons).include.something.that.deep.equals({ - type: 'EXPENSE.NOT.FOUND', code: 100, + expect(res.body.errors).include.something.that.deep.equals({ + type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100, }); }); it('Should response success in case expense transaction was exist.', async () => { const expense = await create('expense'); const res = await request() - .delete(`/api/expense/${expense.id}`) + .delete(`/api/expenses/${expense.id}`) .set('x-access-token', loginRes.body.token) .send(); @@ -162,7 +194,7 @@ describe('routes: /expenses/', () => { it('Should delete the expense transaction from the storage.', async () => { const expense = await create('expense'); await request() - .delete(`/api/expense/${expense.id}`) + .delete(`/api/expenses/${expense.id}`) .set('x-access-token', loginRes.body.token) .send(); @@ -187,6 +219,10 @@ describe('routes: /expenses/', () => { it('Should reverse accounts balance that associated to expense transaction.', () => { }); + + it('Should delete the custom fields that associated to resource and resource item.', () => { + + }); }); describe('POST: `/expenses/bulk`', () => { @@ -296,6 +332,75 @@ describe('routes: /expenses/', () => { }); }); + describe('POST: `/expenses/:id/publish`', () => { + it('Should response not found in case the expense id was not exist.', async () => { + const expense = await create('expense', { published: false }); + const res = await request() + .post('/api/expenses/100/publish') + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'EXPENSE.NOT.FOUND', code: 100, + }); + }); + + it('Should response bad request in case expense is already published.', async () => { + const expense = await create('expense', { published: true }); + const res = await request() + .post(`/api/expenses/${expense.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'EXPENSE.ALREADY.PUBLISHED', code: 200, + }); + }); + + it('Should publish the expense transaction.', async () => { + const expense = await create('expense', { published: false }); + const res = await request() + .post(`/api/expenses/${expense.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + const storedExpense = await Expense.query().findById(expense.id); + expect(storedExpense.published).equals(1); + }); + + it('Should publish the journal entries that associated to the given expense transaction.', async () => { + const expense = await create('expense', { published: false }); + const transaction = await create('account_transaction', { + reference_id: expense.id, + reference_type: 'Expense', + }); + const res = await request() + .post(`/api/expenses/${expense.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + const entries = await AccountTransaction.query() + .where('reference_id', expense.id) + .where('reference_type', 'Expense'); + + entries.forEach((entry) => { + expect(entry.draft).equals(0); + }); + }); + + it('Should response success in case expense was exist and not published.', async () => { + const expense = await create('expense', { published: false }); + const res = await request() + .post(`/api/expenses/${expense.id}/publish`) + .set('x-access-token', loginRes.body.token) + .send(); + + expect(res.status).equals(200); + }); + }); + describe('GET: `/expenses/:id`', () => { it('Should response view not found in case the custom view id was not exist.', async () => { const res = await request() @@ -305,5 +410,9 @@ describe('routes: /expenses/', () => { console.log(res.status); }); + + it('Should retrieve custom fields metadata.', () => { + + }); }); -}); \ No newline at end of file +}); diff --git a/server/tests/routes/fields.test.js b/server/tests/routes/fields.test.js index 5c6229acb..3528fcbfc 100644 --- a/server/tests/routes/fields.test.js +++ b/server/tests/routes/fields.test.js @@ -1,42 +1,93 @@ -import { create, expect, request } from '~/testInit'; +import { + create, + expect, + request, + login, +} from '~/testInit'; import knex from '@/database/knex'; +import ResourceField from '@/models/ResourceField'; +import e from 'express'; +import Fields from '../../src/http/controllers/Fields'; + +let loginRes; describe('route: `/fields`', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); describe('POST: `/fields/:resource_id`', () => { - it('Should `label` be required.', async () => { - const resource = await create('resource'); - const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send(); + it('Should response unauthorized in case the user was not authorized.', async () => { + const res = await request() + .post('/api/fields/resource/items') + .send(); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); + expect(res.body.code).equals('validation_error'); + }); - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('label'); + it('Should response bad request in case resource name was not exist.', async () => { + const res = await request() + .post('/api/fields/resource/not_found_resource') + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Extra Field', + data_type: 'text', + }); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'RESOURCE_NOT_FOUND', code: 100, + }); + }); + + it('Should `label` be required.', async () => { + const resource = await create('resource'); + const res = await request() + .post(`/api/fields/resource/${resource.resource_name}`) + .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: 'label', location: 'body', + }); }); it('Should `data_type` be required.', async () => { const resource = await create('resource'); - const res = await request().post(`/api/fields/resource/${resource.resource_id}`); + const res = await request() + .post(`/api/fields/resource/${resource.resource_id}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + }); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); - - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('data_type'); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'data_type', location: 'body', + }); }); it('Should `data_type` be one in the list.', async () => { const resource = await create('resource'); - const res = await request().post(`/api/fields/resource/${resource.resource_id}`).send({ - label: 'Field label', - data_type: 'invalid_type', - }); + const res = await request() + .post(`/api/fields/resource/${resource.resource_id}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'invalid_type', + }); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); - - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('data_type'); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'data_type', location: 'body', value: 'invalid_type', + }); }); it('Should `value` be boolean valid value in case `data_type` was `boolean`.', () => { @@ -63,13 +114,16 @@ describe('route: `/fields`', () => { }); - it('Should response not found in case resource id was not exist.', async () => { - const res = await request().post('/api/fields/resource/100').send({ - label: 'Field label', - data_type: 'text', - default: 'default value', - help_text: 'help text', - }); + it('Should response not found in case resource name was not exist.', async () => { + const res = await request() + .post('/api/fields/resource/resource_not_found') + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + }); expect(res.status).equals(404); expect(res.body.errors).include.something.that.deep.equals({ @@ -77,63 +131,81 @@ describe('route: `/fields`', () => { }); }); - it('Should response success with valid data.', async () => { const resource = await create('resource'); - const res = await request().post(`/api/fields/resource/${resource.id}`).send({ - label: 'Field label', - data_type: 'text', - default: 'default value', - help_text: 'help text', - }); + const res = await request() + .post(`/api/fields/resource/${resource.name}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + }); expect(res.status).equals(200); }); it('Should store the given field details to the storage.', async () => { const resource = await create('resource'); - await request().post(`/api/fields/resource/${resource.id}`).send({ - label: 'Field label', - data_type: 'text', - default: 'default value', - help_text: 'help text', - options: ['option 1', 'option 2'], - }); + const res = await request() + .post(`/api/fields/resource/${resource.name}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + options: ['option 1', 'option 2'], + }); - const foundField = await knex('resource_fields').first(); + const foundField = await ResourceField.query().findById(res.body.id); - expect(foundField.label_name).equals('Field label'); - expect(foundField.data_type).equals('text'); + expect(foundField.labelName).equals('Field label'); + expect(foundField.dataType).equals('text'); expect(foundField.default).equals('default value'); - expect(foundField.help_text).equals('help text'); - expect(foundField.options).equals.deep([ - { key: 1, value: 'option 1' }, - { key: 2, value: 'option 2' }, - ]); + expect(foundField.helpText).equals('help text'); + expect(foundField.options.length).equals(2); }); }); describe('POST: `/fields/:field_id`', () => { - it('Should `label` be required.', async () => { + it('Should response unauthorized in case the user was not authorized.', async () => { const field = await create('resource_field'); - const res = await request().post(`/api/fields/${field.id}`).send(); + const res = await request() + .post(`/api/fields/${field.id}`) + .send(); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); + expect(res.body.code).equals('validation_error'); + }); - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('label'); + it('Should `label` be required.', async () => { + const field = await create('resource_field'); + const res = await request() + .post(`/api/fields/${field.id}`) + .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: 'label', location: 'body', + }) }); it('Should `data_type` be required.', async () => { const field = await create('resource_field'); - const res = await request().post(`/api/fields/${field.id}`); + const res = await request() + .post(`/api/fields/${field.id}`) + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); - - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('data_type'); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'data_type', location: 'body', + }); }); it('Should `data_type` be one in the list.', async () => { @@ -144,28 +216,78 @@ describe('route: `/fields`', () => { }); expect(res.status).equals(422); - expect(res.body.code).equals('VALIDATION_ERROR'); - - const paramsErrors = res.body.errors.map((er) => er.param); - expect(paramsErrors).to.include('data_type'); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + value: 'invalid_type', + msg: 'Invalid value', + param: 'data_type', + location: 'body', + }); }); - it('Should response not found in case resource id was not exist.', async () => { - const res = await request().post('/api/fields/100').send({ - label: 'Field label', - data_type: 'text', - default: 'default value', - help_text: 'help text', - }); + it('Should response not found in case resource field id was not exist.', async () => { + const res = await request() + .post('/api/fields/100') + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + }); expect(res.status).equals(404); expect(res.body.errors).include.something.that.deep.equals({ type: 'FIELD_NOT_FOUND', code: 100, }); }); + + it('Should update details of the given resource field.', async () => { + const field = await create('resource_field'); + const res = await request() + .post(`/api/fields/${field.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + }); - it('Should save the new options of the field in the storage.', async () => { + const updateField = await ResourceField.query().findById(res.body.id); + expect(updateField.labelName).equals('Field label'); + expect(updateField.dataType).equals('text'); + expect(updateField.default).equals('default value'); + expect(updateField.helpText).equals('help text'); + }); + + it('Should save the new options of the field with exist ones in the storage.', async () => { + const field = await create('resource_field', { + options: JSON.stringify([{ key: 1, value: 'Option 1' }]), + }); + const res = await request() + .post(`/api/fields/${field.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + label: 'Field label', + data_type: 'text', + default: 'default value', + help_text: 'help text', + options: [ + { key: 1, value: 'Value Key 1' }, + { key: 2, value: 'Value Key 2' }, + ], + }); + + const updateField = await ResourceField.query().findById(res.body.id); + + expect(updateField.options.length).equals(2); + expect(updateField.options[0].key).equals(1); + expect(updateField.options[1].key).equals(2); + + expect(updateField.options[0].value).equals('Value Key 1'); + expect(updateField.options[1].value).equals('Value Key 2'); }); }); @@ -178,10 +300,12 @@ describe('route: `/fields`', () => { it('Should change status activation of the given field.', async () => { const field = await create('resource_field'); - await request().post(`/api/fields/status/${field.id}`).send({ - active: false, - }); - + const res = await request() + .post(`/api/fields/status/${field.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + active: false, + }); const storedField = await knex('resource_fields').where('id', field.id).first(); expect(storedField.active).equals(0); }); diff --git a/server/tests/routes/items.test.js b/server/tests/routes/items.test.js index 61f4d1f43..951124db4 100644 --- a/server/tests/routes/items.test.js +++ b/server/tests/routes/items.test.js @@ -5,11 +5,23 @@ import { login, } from '~/testInit'; import knex from '@/database/knex'; +import Item from '@/models/Item'; + +let loginRes; describe('routes: `/items`', () => { + beforeEach(async () => { + loginRes = await login(); + }); + afterEach(() => { + loginRes = null; + }); describe('POST: `/items`', () => { it('Should not create a new item if the user was not authorized.', async () => { - const res = await request().post('/api/items').send(); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(401); expect(res.body.message).equals('unauthorized'); @@ -24,71 +36,101 @@ describe('routes: `/items`', () => { }); it('Should `name` be required.', async () => { - const res = await request().post('/api/items').send(); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundNameParam = res.body.errors.find((error) => error.param === 'name'); - expect(!!foundNameParam).equals(true); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'name', location: 'body', + }); }); it('Should `type_id` be required.', async () => { - const res = await request().post('/api/items').send(); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundTypeParam = res.body.errors.find((error) => error.param === 'type_id'); - expect(!!foundTypeParam).equals(true); + expect(res.body.errors).include.something.deep.equals({ + msg: 'Invalid value', param: 'type_id', location: 'body', + }); }); it('Should `buy_price` be numeric.', async () => { - const res = await request().post('/api/items').send({ - buy_price: 'not_numeric', - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + buy_price: 'not_numeric', + }); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundBuyPrice = res.body.errors.find((error) => error.param === 'buy_price'); - expect(!!foundBuyPrice).equals(true); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'buy_price', + location: 'body', + }); }); it('Should `cost_price` be numeric.', async () => { - const res = await request().post('/api/items').send({ - cost_price: 'not_numeric', - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + cost_price: 'not_numeric', + }); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundCostParam = res.body.errors.find((error) => error.param === 'cost_price'); - expect(!!foundCostParam).equals(true); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'cost_price', + location: 'body', + }); }); - it('Should `buy_account_id` be integer.', async () => { - const res = await request().post('/api/items').send({ - buy_account_id: 'not_numeric', - }); + it('Should `sell_account_id` be integer.', async () => { + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + sell_account_id: 'not_numeric', + }); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundAccount = res.body.errors.find((error) => error.param === 'buy_account_id'); - expect(!!foundAccount).equals(true); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'sell_account_id', + location: 'body', + }); }); it('Should `cost_account_id` be integer.', async () => { - const res = await request().post('/api/items').send({ - cost_account_id: 'not_numeric', - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + cost_account_id: 'not_numeric', + }); expect(res.status).equals(422); expect(res.body.code).equals('validation_error'); - - const foundAccount = res.body.errors.find((error) => error.param === 'cost_account_id'); - expect(!!foundAccount).equals(true); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'cost_account_id', + location: 'body', + }); }); it('Should `cost_account_id` be required if `cost_price` was presented.', async () => { @@ -100,14 +142,17 @@ describe('routes: `/items`', () => { }); it('Should response bad request in case cost account was not exist.', async () => { - const res = await request().post('/api/items').send({ - name: 'Item Name', - type_id: 1, - buy_price: 10.2, - cost_price: 20.2, - sell_account_id: 10, - cost_account_id: 20, - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type_id: 1, + buy_price: 10.2, + cost_price: 20.2, + sell_account_id: 10, + cost_account_id: 20, + }); expect(res.status).equals(400); expect(res.body.errors).include.something.that.deep.equals({ @@ -116,14 +161,17 @@ describe('routes: `/items`', () => { }); it('Should response bad request in case sell account was not exist.', async () => { - const res = await request().post('/api/items').send({ - name: 'Item Name', - type_id: 1, - buy_price: 10.2, - cost_price: 20.2, - sell_account_id: 10, - cost_account_id: 20, - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type_id: 1, + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: 10, + cost_account_id: 20, + }); expect(res.status).equals(400); expect(res.body.errors).include.something.that.deep.equals({ @@ -132,15 +180,18 @@ describe('routes: `/items`', () => { }); it('Should response not category found in case item category was not exist.', async () => { - const res = await request().post('/api/items').send({ - name: 'Item Name', - type_id: 1, - buy_price: 10.2, - cost_price: 20.2, - sell_account_id: 10, - cost_account_id: 20, - category_id: 20, - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type_id: 1, + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: 10, + cost_account_id: 20, + category_id: 20, + }); expect(res.status).equals(400); expect(res.body.errors).include.something.that.deep.equals({ @@ -153,40 +204,248 @@ describe('routes: `/items`', () => { const anotherAccount = await create('account'); const itemCategory = await create('item_category'); - const res = await request().post('/api/items').send({ - name: 'Item Name', - type_id: 1, - buy_price: 10.2, - cost_price: 20.2, - sell_account_id: account.id, - cost_account_id: anotherAccount.id, - category_id: itemCategory.id, - }); + const res = await request() + .post('/api/items') + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type_id: 1, + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: account.id, + cost_account_id: anotherAccount.id, + category_id: itemCategory.id, + }); expect(res.status).equals(200); }); }); + describe('POST: `items/:id`', () => { + it('Should response item not found in case item id was not exist.', async () => { + const res = await request() + .post('/api/items/100') + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type: 'product', + cost_price: 100, + sell_price: 200, + sell_account_id: 1, + cost_account_id: 2, + category_id: 2, + }); + + expect(res.status).equals(404); + expect(res.body.errors).include.something.deep.equals({ + type: 'ITEM.NOT.FOUND', code: 100, + }); + }); + + it('Should `name` be required.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .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: 'name', location: 'body', + }); + }); + + it('Should `type` be required.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .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: 'type', location: 'body', + }); + }); + + it('Should `sell_price` be numeric.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + sell_price: 'not_numeric', + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'sell_price', + location: 'body', + }); + }); + + it('Should `cost_price` be numeric.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + cost_price: 'not_numeric', + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'cost_price', + location: 'body', + }); + }); + + it('Should `sell_account_id` be integer.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + sell_account_id: 'not_numeric', + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'sell_account_id', + location: 'body', + }); + }); + + it('Should `cost_account_id` be integer.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + cost_account_id: 'not_numeric', + }); + + expect(res.status).equals(422); + expect(res.body.code).equals('validation_error'); + expect(res.body.errors).include.something.deep.equals({ + value: 'not_numeric', + msg: 'Invalid value', + param: 'cost_account_id', + location: 'body', + }); + }); + + it('Should response bad request in case cost account was not exist.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type: 'service', + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: 10, + cost_account_id: 20, + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'COST_ACCOUNT_NOT_FOUND', code: 100, + }); + }); + + it('Should response bad request in case sell account was not exist.', async () => { + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + name: 'Item Name', + type: 'product', + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: 10, + cost_account_id: 20, + }); + + expect(res.status).equals(400); + expect(res.body.errors).include.something.that.deep.equals({ + type: 'SELL_ACCOUNT_NOT_FOUND', code: 120, + }); + }); + + it('Should update details of the given item.', async () => { + const account = await create('account'); + const anotherAccount = await create('account'); + const itemCategory = await create('item_category'); + + const item = await create('item'); + const res = await request() + .post(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send({ + name: 'New Item Name', + type: 'service', + sell_price: 10.2, + cost_price: 20.2, + sell_account_id: account.id, + cost_account_id: anotherAccount.id, + category_id: itemCategory.id, + }); + + const updatedItem = await Item.query().findById(item.id); + + expect(updatedItem.name).equals('New Item Name'); + expect(updatedItem.type).equals('service'); + expect(updatedItem.sellPrice).equals(10.2); + expect(updatedItem.costPrice).equals(20.2); + expect(updatedItem.sellAccountId).equals(account.id); + expect(updatedItem.costAccountId).equals(anotherAccount.id); + expect(updatedItem.categoryId).equals(itemCategory.id); + }); + }); + describe('DELETE: `items/:id`', () => { it('Should response not found in case the item was not exist.', async () => { - const res = await request().delete('/api/items/10').send(); + const res = await request() + .delete('/api/items/10') + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(404); }); it('Should response success in case was exist.', async () => { const item = await create('item'); - const res = await request().delete(`/api/items/${item.id}`); + const res = await request() + .delete(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send(); expect(res.status).equals(200); }); it('Should delete the given item from the storage.', async () => { const item = await create('item'); - await request().delete(`/api/items/${item.id}`); + await request() + .delete(`/api/items/${item.id}`) + .set('x-access-token', loginRes.body.token) + .send(); - const storedItem = await knex('items').where('id', item.id); - expect(storedItem).to.have.lengthOf(0); + const storedItems = await Item.query().where('id', item.id); + expect(storedItems).to.have.lengthOf(0); }); }); });