mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
WIP Financial accounting module.
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
]);
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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());
|
||||
|
||||
10
server/src/models/ManualJournal.js
Normal file
10
server/src/models/ManualJournal.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import BaseModel from '@/models/Model';
|
||||
|
||||
export default class ManualJournal extends BaseModel {
|
||||
/**
|
||||
* Table name.
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'manual_journals';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
39
server/src/models/ResourceFieldMetadata.js
Normal file
39
server/src/models/ResourceFieldMetadata.js
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,4 @@ import { extendMoment } from 'moment-range';
|
||||
|
||||
const moment = extendMoment(Moment);
|
||||
|
||||
export default moment;
|
||||
export default moment;
|
||||
|
||||
97
server/tests/lib/ResourceCustomFieldRepository.test.js
Normal file
97
server/tests/lib/ResourceCustomFieldRepository.test.js
Normal file
@@ -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', () => {
|
||||
|
||||
// });
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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.', () => {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user