mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
feat: Expenses resource.
This commit is contained in:
@@ -25,7 +25,7 @@ export default {
|
||||
'type': {
|
||||
column: 'account_type_id',
|
||||
relation: 'account_types.id',
|
||||
relationColumn: 'account_types.name',
|
||||
relationColumn: 'account_types.id',
|
||||
},
|
||||
'description': {
|
||||
column: 'description',
|
||||
@@ -38,6 +38,9 @@ export default {
|
||||
relation: 'account_types.id',
|
||||
relationColumn: 'account_types.root_type',
|
||||
},
|
||||
'created_at': {
|
||||
column: 'created_at',
|
||||
},
|
||||
},
|
||||
|
||||
// Items
|
||||
|
||||
@@ -97,6 +97,7 @@ export default (tenantDb) => {
|
||||
const costAccount = await factory.create('account');
|
||||
const sellAccount = await factory.create('account');
|
||||
const inventoryAccount = await factory.create('account');
|
||||
|
||||
return {
|
||||
name: faker.lorem.word(),
|
||||
note: faker.lorem.paragraph(),
|
||||
@@ -222,17 +223,30 @@ export default (tenantDb) => {
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('expense', 'expenses', async () => {
|
||||
factory.define('expense', 'expenses_transactions', async () => {
|
||||
const paymentAccount = await factory.create('account');
|
||||
const expenseAccount = await factory.create('account');
|
||||
const user = await factory.create('user');
|
||||
|
||||
return {
|
||||
payment_account_id: paymentAccount.id,
|
||||
expense_account_id: expenseAccount.id,
|
||||
user_id: user.id,
|
||||
amount: faker.random.number(),
|
||||
total_amount: faker.random.number(),
|
||||
currency_code: 'USD',
|
||||
description: '',
|
||||
reference_no: faker.random.number(),
|
||||
payment_account_id: paymentAccount.id,
|
||||
published: true,
|
||||
user_id: user.id,
|
||||
};
|
||||
});
|
||||
|
||||
factory.define('expense_category', 'expense_transaction_categories', async () => {
|
||||
const expense = await factory.create('expense');
|
||||
|
||||
return {
|
||||
expense_account_id: expense.id,
|
||||
description: '',
|
||||
amount: faker.random.number(),
|
||||
expense_id: expense.id,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('expenses', (table) => {
|
||||
return knex.schema.createTable('expenses_transactions', (table) => {
|
||||
table.increments();
|
||||
table.decimal('amount');
|
||||
table.decimal('total_amount');
|
||||
table.string('currency_code');
|
||||
table.decimal('exchange_rate');
|
||||
table.text('description');
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('payment_account_id').unsigned();
|
||||
table.string('reference');
|
||||
table.integer('payee_id').unsigned();
|
||||
table.string('reference_no');
|
||||
table.boolean('published').defaultTo(false);
|
||||
table.integer('user_id').unsigned();
|
||||
table.date('date');
|
||||
// table.timestamps();
|
||||
})
|
||||
table.date('payment_date');
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSES_TRANSACTIONS` AUTO_INCREMENT = 1000');
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
return knex.schema.createTable('expense_transaction_categories', table => {
|
||||
table.increments();
|
||||
table.integer('expense_account_id').unsigned();
|
||||
table.integer('index').unsigned();
|
||||
table.text('description');
|
||||
table.decimal('amount');
|
||||
table.integer('expense_id').unsigned();
|
||||
table.timestamps();
|
||||
}).raw('ALTER TABLE `EXPENSE_TRANSACTION_CATEGORIES` AUTO_INCREMENT = 1000');;
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
return knex.schema.dropTableIfExists('expense_transaction_categories');
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
exports.up = function(knex) {
|
||||
|
||||
};
|
||||
|
||||
exports.down = function(knex) {
|
||||
|
||||
};
|
||||
@@ -49,6 +49,15 @@ exports.seed = (knex) => {
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
{
|
||||
id: 16,
|
||||
resource_id: 1,
|
||||
label_name: 'Created at',
|
||||
data_type: 'date',
|
||||
key: 'created_at',
|
||||
predefined: 1,
|
||||
columnable: true,
|
||||
},
|
||||
|
||||
// Expenses
|
||||
{
|
||||
|
||||
@@ -6,16 +6,21 @@ import {
|
||||
validationResult,
|
||||
} from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { difference, chain, omit } from 'lodash';
|
||||
import { difference, sumBy, omit } from 'lodash';
|
||||
import asyncMiddleware from '@/http/middleware/asyncMiddleware';
|
||||
import JournalPoster from '@/services/Accounting/JournalPoster';
|
||||
import JournalEntry from '@/services/Accounting/JournalEntry';
|
||||
import JWTAuth from '@/http/middleware/jwtAuth';
|
||||
import ResourceCustomFieldRepository from '@/services/CustomFields/ResourceCustomFieldRepository';
|
||||
import {
|
||||
validateViewRoles,
|
||||
mapViewRolesToConditionals,
|
||||
} from '@/lib/ViewRolesBuilder';
|
||||
import {
|
||||
DynamicFilter,
|
||||
DynamicFilterSortBy,
|
||||
DynamicFilterViews,
|
||||
DynamicFilterFilterRoles,
|
||||
} from '@/lib/DynamicFilter';
|
||||
|
||||
|
||||
export default {
|
||||
/**
|
||||
@@ -37,10 +42,6 @@ export default {
|
||||
this.deleteExpense.validation,
|
||||
asyncMiddleware(this.deleteExpense.handler));
|
||||
|
||||
router.post('/bulk',
|
||||
this.bulkAddExpenses.validation,
|
||||
asyncMiddleware(this.bulkAddExpenses.handler));
|
||||
|
||||
router.post('/:id',
|
||||
this.updateExpense.validation,
|
||||
asyncMiddleware(this.updateExpense.handler));
|
||||
@@ -49,9 +50,9 @@ export default {
|
||||
this.listExpenses.validation,
|
||||
asyncMiddleware(this.listExpenses.handler));
|
||||
|
||||
// router.get('/:id',
|
||||
// this.getExpense.validation,
|
||||
// asyncMiddleware(this.getExpense.handler));
|
||||
router.get('/:id',
|
||||
this.getExpense.validation,
|
||||
asyncMiddleware(this.getExpense.handler));
|
||||
|
||||
return router;
|
||||
},
|
||||
@@ -61,15 +62,21 @@ export default {
|
||||
*/
|
||||
newExpense: {
|
||||
validation: [
|
||||
check('date').optional(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('payment_date').isISO8601().optional(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expense_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
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('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.index').exists().isNumeric().toInt(),
|
||||
check('categories.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('categories.*.amount').optional().isNumeric().toFloat(),
|
||||
check('categories.*.description').optional().trim().escape(),
|
||||
|
||||
check('custom_fields').optional().isArray({ min: 0 }),
|
||||
check('custom_fields.*.key').exists().trim().escape(),
|
||||
check('custom_fields.*.value').exists(),
|
||||
],
|
||||
@@ -81,170 +88,94 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { user } = req;
|
||||
const { Expense, ExpenseCategory, Account } = req.models;
|
||||
|
||||
const form = {
|
||||
date: new Date(),
|
||||
published: false,
|
||||
custom_fields: [],
|
||||
categories: [],
|
||||
...req.body,
|
||||
};
|
||||
const { Account, Expense } = req.models;
|
||||
// Convert the date to the general format.
|
||||
form.date = moment(form.date).format('YYYY-MM-DD');
|
||||
const totalAmount = sumBy(form.categories, 'amount');
|
||||
const expenseAccountsIds = form.categories.map((account) => account.expense_account_id)
|
||||
|
||||
const storedExpenseAccounts = await Account.query().whereIn('id', expenseAccountsIds);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
|
||||
const notStoredExpensesAccountsIds = difference(expenseAccountsIds, storedExpenseAccountsIds);
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.findById(form.payment_account_id).first();
|
||||
|
||||
const paymentAccount = await Account.query().where('id', form.payment_account_id).first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 100 });
|
||||
errorReasons.push({
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500,
|
||||
});
|
||||
}
|
||||
const expenseAccount = await Account.query().findById(form.expense_account_id).first();
|
||||
|
||||
if (!expenseAccount) {
|
||||
errorReasons.push({ type: 'EXPENSE.ACCOUNT.NOT.FOUND', code: 200 });
|
||||
if (notStoredExpensesAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: notStoredExpensesAccountsIds,
|
||||
});
|
||||
}
|
||||
if (totalAmount <= 0) {
|
||||
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 300 });
|
||||
}
|
||||
// const customFields = new ResourceCustomFieldRepository(Expense);
|
||||
// await customFields.load();
|
||||
|
||||
// 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().insertAndFetch({
|
||||
...omit(form, ['custom_fields']),
|
||||
|
||||
const expenseTransaction = await Expense.query().insert({
|
||||
total_amount: totalAmount,
|
||||
payment_account_id: form.payment_account_id,
|
||||
reference_no: form.reference_no,
|
||||
description: form.description,
|
||||
payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
|
||||
user_id: user.id,
|
||||
});
|
||||
// customFields.fillCustomFields(expenseTransaction.id, form.custom_fields);
|
||||
const storeExpenseCategoriesOper = [];
|
||||
|
||||
const journalEntries = new JournalPoster();
|
||||
const creditEntry = new JournalEntry({
|
||||
credit: form.amount,
|
||||
form.categories.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().insert({
|
||||
expense_id: expenseTransaction.id,
|
||||
...category,
|
||||
});
|
||||
storeExpenseCategoriesOper.push(oper);
|
||||
});
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query();
|
||||
const journalPoster = new JournalPoster(accountsDepGraph);
|
||||
|
||||
const mixinEntry = {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expenseTransaction.id,
|
||||
referenceType: Expense.referenceType,
|
||||
date: form.date,
|
||||
account: expenseAccount.id,
|
||||
accountNormal: 'debit',
|
||||
draft: !form.published,
|
||||
});
|
||||
const debitEntry = new JournalEntry({
|
||||
debit: form.amount,
|
||||
referenceId: expenseTransaction.id,
|
||||
referenceType: Expense.referenceType,
|
||||
date: form.date,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
account: paymentAccount.id,
|
||||
accountNormal: 'debit',
|
||||
draft: !form.published,
|
||||
...mixinEntry,
|
||||
});
|
||||
journalEntries.credit(creditEntry);
|
||||
journalEntries.debit(debitEntry);
|
||||
journalPoster.credit(paymentJournalEntry)
|
||||
|
||||
form.categories.forEach((category) => {
|
||||
const expenseJournalEntry = new JournalEntry({
|
||||
account: category.expense_account_id,
|
||||
debit: category.amount,
|
||||
note: category.description,
|
||||
...mixinEntry,
|
||||
});
|
||||
journalPoster.debit(expenseJournalEntry);
|
||||
});
|
||||
await Promise.all([
|
||||
// customFields.saveCustomFields(expenseTransaction.id),
|
||||
journalEntries.saveEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id: expenseTransaction.id });
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk add expneses to the given accounts.
|
||||
*/
|
||||
bulkAddExpenses: {
|
||||
validation: [
|
||||
check('expenses').exists().isArray({ min: 1 }),
|
||||
check('expenses.*.date').optional().isISO8601(),
|
||||
check('expenses.*.payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expenses.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('expenses.*.description').optional(),
|
||||
check('expenses.*.amount').exists().isNumeric().toFloat(),
|
||||
check('expenses.*.currency_code').optional(),
|
||||
check('expenses.*.exchange_rate').optional().isNumeric().toFloat(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
const { Account, Expense } = req.models;
|
||||
const form = { ...req.body };
|
||||
const errorReasons = [];
|
||||
|
||||
const paymentAccountsIds = chain(form.expenses)
|
||||
.map((e) => e.payment_account_id).uniq().value();
|
||||
const expenseAccountsIds = chain(form.expenses)
|
||||
.map((e) => e.expense_account_id).uniq().value();
|
||||
|
||||
const [expensesAccounts, paymentAccounts] = await Promise.all([
|
||||
Account.query().whereIn('id', expenseAccountsIds),
|
||||
Account.query().whereIn('id', paymentAccountsIds),
|
||||
]);
|
||||
const storedExpensesAccountsIds = expensesAccounts.map((a) => a.id);
|
||||
const storedPaymentAccountsIds = paymentAccounts.map((a) => a.id);
|
||||
|
||||
const notFoundPaymentAccountsIds = difference(expenseAccountsIds, storedExpensesAccountsIds);
|
||||
const notFoundExpenseAccountsIds = difference(paymentAccountsIds, storedPaymentAccountsIds);
|
||||
|
||||
if (notFoundPaymentAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'PAYMENY.ACCOUNTS.NOT.FOUND',
|
||||
code: 100,
|
||||
accounts: notFoundPaymentAccountsIds,
|
||||
});
|
||||
}
|
||||
if (notFoundExpenseAccountsIds.length > 0) {
|
||||
errorReasons.push({
|
||||
type: 'EXPENSE.ACCOUNTS.NOT.FOUND',
|
||||
code: 200,
|
||||
accounts: notFoundExpenseAccountsIds,
|
||||
});
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { reasons: errorReasons });
|
||||
}
|
||||
const expenseSaveOpers = [];
|
||||
const journalPoster = new JournalPoster();
|
||||
|
||||
form.expenses.forEach(async (expense) => {
|
||||
const expenseSaveOper = Expense.query().insert({ ...expense });
|
||||
expenseSaveOpers.push(expenseSaveOper);
|
||||
});
|
||||
// Wait unit save all expense transactions.
|
||||
const savedExpenseTransactions = await Promise.all(expenseSaveOpers);
|
||||
|
||||
savedExpenseTransactions.forEach((expense) => {
|
||||
const date = moment(expense.date).format('YYYY-DD-MM');
|
||||
|
||||
const debit = new JournalEntry({
|
||||
debit: expense.amount,
|
||||
referenceId: expense.id,
|
||||
referenceType: Expense.referenceType,
|
||||
account: expense.payment_account_id,
|
||||
accountNormal: 'debit',
|
||||
date,
|
||||
});
|
||||
const credit = new JournalEntry({
|
||||
credit: expense.amount,
|
||||
referenceId: expense.id,
|
||||
referenceType: Expense.referenceId,
|
||||
account: expense.expense_account_id,
|
||||
accountNormal: 'debit',
|
||||
date,
|
||||
});
|
||||
journalPoster.credit(credit);
|
||||
journalPoster.debit(debit);
|
||||
});
|
||||
|
||||
// Save expense journal entries and balance change.
|
||||
await Promise.all([
|
||||
...storeExpenseCategoriesOper,
|
||||
journalPoster.saveEntries(),
|
||||
journalPoster.saveBalance(),
|
||||
(form.status) && journalPoster.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
|
||||
return res.status(200).send({ id: expenseTransaction.id });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -263,16 +194,13 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { Expense, AccountTransaction } = req.models;
|
||||
const errorReasons = [];
|
||||
const expense = await Expense.query().findById(id);
|
||||
const errorReasons = [];
|
||||
|
||||
if (!expense) {
|
||||
errorReasons.push({ type: 'EXPENSE.NOT.FOUND', code: 100 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
if (expense.published) {
|
||||
@@ -281,18 +209,33 @@ export default {
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Expense'])
|
||||
.where('reference_id', expense.id)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
await AccountTransaction.query()
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.calculateEntriesBalanceChange();
|
||||
|
||||
const updateAccTransactionsOper = AccountTransaction.query()
|
||||
.where('reference_id', expense.id)
|
||||
.where('reference_type', 'Expense')
|
||||
.patch({
|
||||
draft: false,
|
||||
});
|
||||
|
||||
await Expense.query()
|
||||
const updateExpenseOper = Expense.query()
|
||||
.where('id', expense.id)
|
||||
.update({ published: true });
|
||||
|
||||
await Promise.all([
|
||||
updateAccTransactionsOper,
|
||||
updateExpenseOper,
|
||||
journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
},
|
||||
@@ -301,25 +244,15 @@ export default {
|
||||
* Retrieve paginated expenses list.
|
||||
*/
|
||||
listExpenses: {
|
||||
validation: [
|
||||
query('expense_account_id').optional().isNumeric().toInt(),
|
||||
query('payment_account_id').optional().isNumeric().toInt(),
|
||||
query('note').optional(),
|
||||
query('range_from').optional().isNumeric().toFloat(),
|
||||
query('range_to').optional().isNumeric().toFloat(),
|
||||
query('date_from').optional().isISO8601(),
|
||||
query('date_to').optional().isISO8601(),
|
||||
query('column_sort_order').optional().isIn(['created_at', 'date', 'amount']),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
validation: [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('filter_roles').optional().isArray(),
|
||||
query('filter_roles.*.field_key').exists().escape().trim(),
|
||||
query('filter_roles.*.value').exists().escape().trim(),
|
||||
query('filter_roles.*.comparator').exists().escape().trim(),
|
||||
query('filter_roles.*.index').exists().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -329,77 +262,99 @@ export default {
|
||||
code: 'validation_error', ...validationErrors,
|
||||
});
|
||||
}
|
||||
|
||||
const filter = {
|
||||
page_size: 10,
|
||||
sort_order: 'asc',
|
||||
filter_roles: [],
|
||||
page_size: 15,
|
||||
page: 1,
|
||||
...req.query,
|
||||
};
|
||||
const { Resource, View, Expense } = req.models;
|
||||
const errorReasons = [];
|
||||
const expenseResource = await Resource.query().where('name', 'expenses').first();
|
||||
const { Resource, Expense, View } = req.models;
|
||||
|
||||
if (!expenseResource) {
|
||||
errorReasons.push({ type: 'EXPENSE_RESOURCE_NOT_FOUND', code: 300 });
|
||||
const expensesResource = await Resource.query()
|
||||
.remember()
|
||||
.where('name', 'expenses')
|
||||
.withGraphFetched('fields')
|
||||
.first();
|
||||
|
||||
const expensesResourceFields = expensesResource.fields.map(f => f.key);
|
||||
|
||||
if (!expensesResource) {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXPENSES.RESOURCE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const view = await View.query().onBuild((builder) => {
|
||||
if (filter.csutom_view_id) {
|
||||
builder.where('id', filter.csutom_view_id);
|
||||
} else {
|
||||
builder.where('favourite', true);
|
||||
}
|
||||
builder.withGraphFetched('roles.field');
|
||||
builder.withGraphFetched('columns');
|
||||
builder.first();
|
||||
});
|
||||
const dynamicFilter = new DynamicFilter(Expense.tableName);
|
||||
|
||||
// Column sorting.
|
||||
if (filter.column_sort_by) {
|
||||
if (expensesResourceFields.indexOf(filter.column_sort_by) === -1) {
|
||||
errorReasons.push({ type: 'COLUMN.SORT.ORDER.NOT.FOUND', code: 300 });
|
||||
}
|
||||
const sortByFilter = new DynamicFilterSortBy(
|
||||
filter.column_sort_by,
|
||||
filter.sort_order,
|
||||
);
|
||||
dynamicFilter.setFilter(sortByFilter);
|
||||
}
|
||||
// Custom view roles.
|
||||
if (view && view.roles.length > 0) {
|
||||
const viewFilter = new DynamicFilterViews(
|
||||
mapViewRolesToConditionals(view.roles),
|
||||
view.rolesLogicExpression,
|
||||
);
|
||||
if (viewFilter.validateFilterRoles()) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
}
|
||||
dynamicFilter.setFilter(viewFilter);
|
||||
}
|
||||
// Filter roles.
|
||||
if (filter.filter_roles.length > 0) {
|
||||
const filterRoles = new DynamicFilterFilterRoles(
|
||||
mapFilterRolesToDynamicFilter(filter.filter_roles),
|
||||
expensesResource.fields,
|
||||
);
|
||||
if (filterRoles.validateFilterRoles().length > 0) {
|
||||
errorReasons.push({ type: 'ACCOUNTS.RESOURCE.HAS.NO.GIVEN.FIELDS', code: 500 });
|
||||
}
|
||||
dynamicFilter.setFilter(filterRoles);
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const view = await View.query().onBuild((builder) => {
|
||||
if (filter.custom_view_id) {
|
||||
builder.where('id', filter.custom_view_id);
|
||||
} else {
|
||||
builder.where('favourite', true);
|
||||
}
|
||||
builder.where('resource_id', expenseResource.id);
|
||||
builder.withGraphFetched('viewRoles.field');
|
||||
builder.withGraphFetched('columns');
|
||||
|
||||
builder.first();
|
||||
});
|
||||
let viewConditionals = [];
|
||||
|
||||
if (view && view.viewRoles.length > 0) {
|
||||
viewConditionals = mapViewRolesToConditionals(view.viewRoles);
|
||||
|
||||
if (!validateViewRoles(viewConditionals, view.rolesLogicExpression)) {
|
||||
errorReasons.push({ type: 'VIEW.LOGIC.EXPRESSION.INVALID', code: 400 });
|
||||
}
|
||||
}
|
||||
if (!view && filter.custom_view_id) {
|
||||
errorReasons.push({ type: 'VIEW_NOT_FOUND', code: 100 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
|
||||
const expenses = await Expense.query().onBuild((builder) => {
|
||||
builder.withGraphFetched('paymentAccount');
|
||||
builder.withGraphFetched('expenseAccount');
|
||||
builder.withGraphFetched('categories');
|
||||
builder.withGraphFetched('user');
|
||||
|
||||
if (viewConditionals.length) {
|
||||
builder.modify('viewRolesBuilder', viewConditionals, view.rolesLogicExpression);
|
||||
}
|
||||
builder.modify('filterByAmountRange', filter.range_from, filter.to_range);
|
||||
builder.modify('filterByDateRange', filter.date_from, filter.date_to);
|
||||
builder.modify('filterByExpenseAccount', filter.expense_account_id);
|
||||
builder.modify('filterByPaymentAccount', filter.payment_account_id);
|
||||
builder.modify('orderBy', filter.column_sort_order, filter.sort_order);
|
||||
}).page(filter.page - 1, filter.page_size);
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
}).pagination(filter.page - 1, filter.page_size);;
|
||||
|
||||
return res.status(200).send({
|
||||
...(view) ? {
|
||||
customViewId: view.id,
|
||||
viewColumns: view.columns,
|
||||
viewConditionals,
|
||||
} : {},
|
||||
expenses,
|
||||
page_size: filter.page_size,
|
||||
page: filter.page,
|
||||
...(view) ? {
|
||||
viewColumns: view.columns,
|
||||
customViewId: view.id,
|
||||
} : {},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
* Delete the given expense transaction.
|
||||
*/
|
||||
deleteExpense: {
|
||||
validation: [
|
||||
@@ -414,26 +369,37 @@ export default {
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { Expense, AccountTransaction } = req.models;
|
||||
const expenseTransaction = await Expense.query().findById(id);
|
||||
const {
|
||||
Expense,
|
||||
ExpenseCategory,
|
||||
AccountTransaction,
|
||||
Account,
|
||||
} = req.models;
|
||||
|
||||
if (!expenseTransaction) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
const expense = await Expense.query().where('id', id).first();
|
||||
|
||||
if (!expense) {
|
||||
return res.status(404).send({ errors: [{
|
||||
type: 'EXPENSE.NOT.FOUND', code: 200,
|
||||
}] });
|
||||
}
|
||||
const expenseEntries = await AccountTransaction.query()
|
||||
await ExpenseCategory.query().where('expense_id', id).delete();
|
||||
|
||||
const deleteExpenseOper = Expense.query().where('id', id).delete();
|
||||
const expenseTransactions = await AccountTransaction.query()
|
||||
.where('reference_type', 'Expense')
|
||||
.where('reference_id', expenseTransaction.id);
|
||||
.where('reference_id', expense.id);
|
||||
|
||||
const expenseEntriesCollect = new JournalPoster();
|
||||
expenseEntriesCollect.loadEntries(expenseEntries);
|
||||
expenseEntriesCollect.reverseEntries();
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const journalEntries = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journalEntries.loadEntries(expenseTransactions);
|
||||
journalEntries.removeEntries();
|
||||
|
||||
await Promise.all([
|
||||
Expense.query().findById(expenseTransaction.id).delete(),
|
||||
expenseEntriesCollect.deleteEntries(),
|
||||
expenseEntriesCollect.saveBalance(),
|
||||
deleteExpenseOper,
|
||||
journalEntries.deleteEntries(),
|
||||
journalEntries.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send();
|
||||
},
|
||||
@@ -445,13 +411,20 @@ export default {
|
||||
updateExpense: {
|
||||
validation: [
|
||||
param('id').isNumeric().toInt(),
|
||||
check('date').optional().isISO8601(),
|
||||
check('reference_no').optional().trim().escape(),
|
||||
check('payment_date').isISO8601().optional(),
|
||||
check('payment_account_id').exists().isNumeric().toInt(),
|
||||
check('expense_account_id').exists().isNumeric().toInt(),
|
||||
check('description').optional(),
|
||||
check('amount').exists().isNumeric().toFloat(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.id').optional().isNumeric().toInt(),
|
||||
check('categories.*.index').exists().isNumeric().toInt(),
|
||||
check('categories.*.expense_account_id').exists().isNumeric().toInt(),
|
||||
check('categories.*.amount').optional().isNumeric().toFloat(),
|
||||
check('categories.*.description').optional().trim().escape(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const validationErrors = validationResult(req);
|
||||
@@ -462,14 +435,149 @@ export default {
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { Expense } = req.models;
|
||||
const expenseTransaction = await Expense.query().findById(id);
|
||||
const { user } = req;
|
||||
const { Account, Expense, ExpenseCategory, AccountTransaction } = req.models;
|
||||
|
||||
if (!expenseTransaction) {
|
||||
const form = {
|
||||
categories: [],
|
||||
...req.body,
|
||||
};
|
||||
if (!Array.isArray(form.categories)) {
|
||||
form.categories = [form.categories];
|
||||
}
|
||||
const expense = await Expense.query()
|
||||
.where('id', id)
|
||||
.withGraphFetched('categories')
|
||||
.first();
|
||||
|
||||
if (!expense) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
|
||||
errors: [{ type: 'EXPENSE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
const errorReasons = [];
|
||||
const paymentAccount = await Account.query()
|
||||
.where('id', form.payment_account_id).first();
|
||||
|
||||
if (!paymentAccount) {
|
||||
errorReasons.push({ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400 });
|
||||
}
|
||||
const categoriesHasNoId = form.categories.filter(c => !c.id);
|
||||
const categoriesHasId = form.categories.filter(c => c.id);
|
||||
|
||||
const expenseCategoriesIds = expense.categories.map((c) => c.id);
|
||||
const formExpenseCategoriesIds = categoriesHasId.map(c => c.id);
|
||||
|
||||
const categoriesIdsDeleted = difference(
|
||||
formExpenseCategoriesIds, expenseCategoriesIds,
|
||||
);
|
||||
const categoriesShouldDelete = difference(
|
||||
expenseCategoriesIds, formExpenseCategoriesIds,
|
||||
);
|
||||
|
||||
const formExpensesAccountsIds = form.categories.map(c => c.expense_account_id);
|
||||
const storedExpenseAccounts = await Account.query().whereIn('id', formExpensesAccountsIds);
|
||||
const storedExpenseAccountsIds = storedExpenseAccounts.map(a => a.id);
|
||||
|
||||
const expenseAccountsIdsNotFound = difference(
|
||||
formExpensesAccountsIds, storedExpenseAccountsIds,
|
||||
);
|
||||
const totalAmount = sumBy(form.categories, 'amount');
|
||||
|
||||
if (expenseAccountsIdsNotFound.length > 0) {
|
||||
errorReasons.push({ type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND', code: 600, ids: expenseAccountsIdsNotFound })
|
||||
}
|
||||
|
||||
if (categoriesIdsDeleted.length > 0) {
|
||||
errorReasons.push({ type: 'EXPENSE.CATEGORIES.IDS.NOT.FOUND', code: 300 });
|
||||
}
|
||||
if (totalAmount <= 0) {
|
||||
errorReasons.push({ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500 });
|
||||
}
|
||||
// Handle all error reasons.
|
||||
if (errorReasons.length > 0) {
|
||||
return res.status(400).send({ errors: errorReasons });
|
||||
}
|
||||
const expenseCategoriesMap = new Map(expense.categories
|
||||
.map(category => [category.id, category]));
|
||||
|
||||
const categoriesInsertOpers = [];
|
||||
const categoriesUpdateOpers = [];
|
||||
|
||||
categoriesHasNoId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().insert({
|
||||
...category,
|
||||
expense_id: expense.id,
|
||||
});
|
||||
categoriesInsertOpers.push(oper);
|
||||
});
|
||||
|
||||
categoriesHasId.forEach((category) => {
|
||||
const oper = ExpenseCategory.query().where('id', category.id)
|
||||
.patch({
|
||||
...omit(category, ['id']),
|
||||
});
|
||||
categoriesUpdateOpers.push(oper);
|
||||
});
|
||||
|
||||
const updateExpenseOper = Expense.query().where('id', id)
|
||||
.update({
|
||||
payment_date: moment(form.payment_date).format('YYYY-MM-DD'),
|
||||
total_amount: totalAmount,
|
||||
description: form.description,
|
||||
payment_account_id: form.payment_account_id,
|
||||
reference_no: form.reference_no,
|
||||
})
|
||||
|
||||
const deleteCategoriesOper = (categoriesShouldDelete.length > 0) ?
|
||||
ExpenseCategory.query().whereIn('id', categoriesShouldDelete).delete() :
|
||||
Promise.resolve();
|
||||
|
||||
// Update the journal entries.
|
||||
const transactions = await AccountTransaction.query()
|
||||
.whereIn('reference_type', ['Expense'])
|
||||
.where('reference_id', expense.id)
|
||||
.withGraphFetched('account.type');
|
||||
|
||||
const accountsDepGraph = await Account.depGraph().query().remember();
|
||||
const journal = new JournalPoster(accountsDepGraph);
|
||||
|
||||
journal.loadEntries(transactions);
|
||||
journal.removeEntries();
|
||||
|
||||
const mixinEntry = {
|
||||
referenceType: 'Expense',
|
||||
referenceId: expense.id,
|
||||
userId: user.id,
|
||||
draft: !form.publish,
|
||||
};
|
||||
const paymentJournalEntry = new JournalEntry({
|
||||
credit: totalAmount,
|
||||
account: paymentAccount.id,
|
||||
...mixinEntry,
|
||||
});
|
||||
journal.credit(paymentJournalEntry);
|
||||
|
||||
form.categories.forEach((category) => {
|
||||
const entry = new JournalEntry({
|
||||
account: category.expense_account_id,
|
||||
debit: category.amount,
|
||||
note: category.description,
|
||||
...mixinEntry,
|
||||
});
|
||||
journal.debit(entry);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
...categoriesInsertOpers,
|
||||
...categoriesUpdateOpers,
|
||||
updateExpenseOper,
|
||||
deleteCategoriesOper,
|
||||
|
||||
journal.saveEntries(),
|
||||
(form.status) && journal.saveBalance(),
|
||||
]);
|
||||
return res.status(200).send({ id });
|
||||
},
|
||||
},
|
||||
|
||||
@@ -489,26 +597,30 @@ export default {
|
||||
});
|
||||
}
|
||||
const { id } = req.params;
|
||||
const { Expense } = req.models;
|
||||
const expenseTransaction = await Expense.query().findById(id);
|
||||
const { Expense, AccountTransaction } = req.models;
|
||||
|
||||
if (!expenseTransaction) {
|
||||
const expense = await Expense.query()
|
||||
.where('id', id)
|
||||
.withGraphFetched('categories')
|
||||
.withGraphFetched('paymentAccount')
|
||||
.withGraphFetched('user')
|
||||
.first();
|
||||
|
||||
if (!expense) {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXPENSE.TRANSACTION.NOT.FOUND', code: 100 }],
|
||||
errors: [{ type: 'EXPENSE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
|
||||
const expenseCFMetadataRepo = new ResourceCustomFieldRepository(Expense);
|
||||
await expenseCFMetadataRepo.load();
|
||||
await expenseCFMetadataRepo.fetchCustomFieldsMetadata(expenseTransaction.id);
|
||||
|
||||
const expenseCusFieldsMetadata = expenseCFMetadataRepo.getMetadata(expenseTransaction.id);
|
||||
const journalEntries = await AccountTransaction.query()
|
||||
.where('reference_id', expense.id)
|
||||
.where('reference_type', 'Expense');
|
||||
|
||||
return res.status(200).send({
|
||||
...expenseTransaction,
|
||||
custom_fields: [
|
||||
...expenseCusFieldsMetadata.toArray(),
|
||||
],
|
||||
expense: {
|
||||
...expense,
|
||||
journalEntries,
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@ export default {
|
||||
router() {
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/:resource_slug/data',
|
||||
this.resourceData.validation,
|
||||
asyncMiddleware(this.resourceData.handler));
|
||||
|
||||
router.get('/:resource_slug/columns',
|
||||
this.resourceColumns.validation,
|
||||
asyncMiddleware(this.resourceColumns.handler));
|
||||
@@ -23,6 +27,26 @@ export default {
|
||||
return router;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve resource data of the given resource key/slug.
|
||||
*/
|
||||
resourceData: {
|
||||
validation: [
|
||||
param('resource_slug').trim().escape().exists(),
|
||||
],
|
||||
async handler(req, res) {
|
||||
const { AccountType } = req.models;
|
||||
const { resource_slug: resourceSlug } = req.params;
|
||||
|
||||
const data = await AccountType.query();
|
||||
|
||||
return res.status(200).send({
|
||||
data,
|
||||
resource_slug: resourceSlug,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve resource columns of the given resource.
|
||||
*/
|
||||
|
||||
@@ -13,12 +13,12 @@ import Views from '@/http/controllers/Views';
|
||||
// import CustomFields from '@/http/controllers/Fields';
|
||||
import Accounting from '@/http/controllers/Accounting';
|
||||
import FinancialStatements from '@/http/controllers/FinancialStatements';
|
||||
// import Expenses from '@/http/controllers/Expenses';
|
||||
import Expenses from '@/http/controllers/Expenses';
|
||||
import Options from '@/http/controllers/Options';
|
||||
// import Budget from '@/http/controllers/Budget';
|
||||
// import BudgetReports from '@/http/controllers/BudgetReports';
|
||||
import Currencies from '@/http/controllers/Currencies';
|
||||
// import Customers from '@/http/controllers/Customers';
|
||||
import Customers from '@/http/controllers/Customers';
|
||||
// import Suppliers from '@/http/controllers/Suppliers';
|
||||
// import Bills from '@/http/controllers/Bills';
|
||||
// import CurrencyAdjustment from './controllers/CurrencyAdjustment';
|
||||
@@ -52,11 +52,11 @@ export default (app) => {
|
||||
// app.use('/api/fields', CustomFields.router());
|
||||
dashboard.use('/api/items', Items.router());
|
||||
dashboard.use('/api/item_categories', ItemCategories.router());
|
||||
// app.use('/api/expenses', Expenses.router());
|
||||
dashboard.use('/api/expenses', Expenses.router());
|
||||
dashboard.use('/api/financial_statements', FinancialStatements.router());
|
||||
dashboard.use('/api/options', Options.router());
|
||||
// app.use('/api/budget_reports', BudgetReports.router());
|
||||
// app.use('/api/customers', Customers.router());
|
||||
// dashboard.use('/api/customers', Customers.router());
|
||||
// app.use('/api/suppliers', Suppliers.router());
|
||||
// app.use('/api/bills', Bills.router());
|
||||
// app.use('/api/budget', Budget.router());
|
||||
|
||||
@@ -7,7 +7,7 @@ export default class Expense extends TenantModel {
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'expenses';
|
||||
return 'expenses_transactions';
|
||||
}
|
||||
|
||||
static get referenceType() {
|
||||
@@ -62,31 +62,30 @@ export default class Expense extends TenantModel {
|
||||
static get relationMappings() {
|
||||
const Account = require('@/models/Account');
|
||||
const User = require('@/models/TenantUser');
|
||||
const ExpenseCategory = require('@/models/ExpenseCategory');
|
||||
|
||||
return {
|
||||
paymentAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(Account.default),
|
||||
join: {
|
||||
from: 'expenses.paymentAccountId',
|
||||
from: 'expenses_transactions.paymentAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
expenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(Account.default),
|
||||
categories: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: this.relationBindKnex(ExpenseCategory.default),
|
||||
join: {
|
||||
from: 'expenses.expenseAccountId',
|
||||
to: 'accounts.id',
|
||||
from: 'expenses_transactions.id',
|
||||
to: 'expense_transaction_categories.expenseId',
|
||||
},
|
||||
},
|
||||
|
||||
user: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(User.default),
|
||||
join: {
|
||||
from: 'expenses.userId',
|
||||
from: 'expenses_transactions.userId',
|
||||
to: 'users.id',
|
||||
},
|
||||
},
|
||||
|
||||
29
server/src/models/ExpenseCategory.js
Normal file
29
server/src/models/ExpenseCategory.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from '@/models/TenantModel';
|
||||
|
||||
export default class ExpenseCategory extends TenantModel {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'expense_transaction_categories';
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Account = require('@/models/Account');
|
||||
|
||||
return {
|
||||
expenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: this.relationBindKnex(Account.default),
|
||||
join: {
|
||||
from: 'expense_transaction_categories.expenseAccountId',
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
39
server/tests/models/Expense.test.js
Normal file
39
server/tests/models/Expense.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { create, expect } from '~/testInit';
|
||||
import Expense from '@/models/Expense';
|
||||
import ExpenseCategory from '@/models/ExpenseCategory';
|
||||
import {
|
||||
tenantFactory,
|
||||
tenantWebsite
|
||||
} from '~/dbInit';
|
||||
|
||||
describe('Model: Expense', () => {
|
||||
describe('relations', () => {
|
||||
it('Expense model may belongs to associated payment account.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
|
||||
const expenseModel = await Expense.tenant().query().findById(expense.id);
|
||||
const paymentAccountModel = await expenseModel.$relatedQuery('paymentAccount');
|
||||
|
||||
expect(paymentAccountModel.id).equals(expense.paymentAccountId);
|
||||
});
|
||||
|
||||
it('Expense model may has many associated expense categories.', async () => {
|
||||
const expenseCategory = await tenantFactory.create('expense_category');
|
||||
|
||||
const expenseModel = await Expense.tenant().query().findById(expenseCategory.expenseId);
|
||||
const expenseCategories = await expenseModel.$relatedQuery('categories');
|
||||
|
||||
expect(expenseCategories.length).equals(1);
|
||||
expect(expenseCategories[0].expenseId).equals(expenseModel.id);
|
||||
});
|
||||
|
||||
it('Expense model may belongs to associated user model.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
|
||||
const expenseModel = await Expense.tenant().query().findById(expense.id);
|
||||
const expenseUserModel = await expenseModel.$relatedQuery('user');
|
||||
|
||||
expect(expenseUserModel.id).equals(expense.userId);
|
||||
});
|
||||
});
|
||||
});
|
||||
5
server/tests/models/ExpenseCategory.test.js
Normal file
5
server/tests/models/ExpenseCategory.test.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
describe('ExpenseCategory', () => {
|
||||
|
||||
});
|
||||
681
server/tests/routes/expenses.test.js
Normal file
681
server/tests/routes/expenses.test.js
Normal file
@@ -0,0 +1,681 @@
|
||||
import moment from 'moment';
|
||||
import { pick } from 'lodash';
|
||||
import {
|
||||
request,
|
||||
expect,
|
||||
} from '~/testInit';
|
||||
import Expense from '@/models/Expense';
|
||||
import ExpenseCategory from '@/models/ExpenseCategory';
|
||||
import AccountTransaction from '@/models/AccountTransaction';
|
||||
import {
|
||||
tenantWebsite,
|
||||
tenantFactory,
|
||||
loginRes,
|
||||
} from '~/dbInit';
|
||||
|
||||
describe('routes: /expenses/', () => {
|
||||
describe('POST `/expenses`', () => {
|
||||
it('Should retrieve unauthorized access if the user was not authorized.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should categories total not be equals zero.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
payment_account_id: 0,
|
||||
description: '',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: 33,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: [33]
|
||||
});
|
||||
});
|
||||
|
||||
it('Should expense accounts ids be stored in the storage.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
payment_account_id: 0,
|
||||
description: '',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: 22,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.STORED', code: 400, ids: [22],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should `payment_account_id` be in the storage.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: '',
|
||||
payment_account_id: 22,
|
||||
description: '',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: 22,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should payment_account be required.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(422);
|
||||
});
|
||||
|
||||
it('Should `categories.*.expense_account_id` be required.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
|
||||
});
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
msg: 'Invalid value', param: 'payment_account_id', location: 'body'
|
||||
});
|
||||
});
|
||||
|
||||
it('Should expense transactions be stored on the storage.', async () => {
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: 'ABC',
|
||||
payment_account_id: paymentAccount.id,
|
||||
description: 'desc',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundExpense = await Expense.tenant().query().where('id', res.body.id);
|
||||
|
||||
expect(foundExpense.length).equals(1);
|
||||
expect(foundExpense[0].referenceNo).equals('ABC');
|
||||
expect(foundExpense[0].paymentAccountId).equals(paymentAccount.id);
|
||||
expect(foundExpense[0].description).equals('desc');
|
||||
expect(foundExpense[0].totalAmount).equals(1000);
|
||||
});
|
||||
|
||||
it('Should expense categories transactions be stored on the storage.', async () => {
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: 'ABC',
|
||||
payment_account_id: paymentAccount.id,
|
||||
description: 'desc',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 1000,
|
||||
description: 'category desc',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundCategories = await ExpenseCategory.tenant().query().where('id', res.body.id);
|
||||
|
||||
expect(foundCategories.length).equals(1);
|
||||
expect(foundCategories[0].index).equals(1);
|
||||
expect(foundCategories[0].expenseAccountId).equals(expenseAccount.id);
|
||||
expect(foundCategories[0].amount).equals(1000);
|
||||
expect(foundCategories[0].description).equals('category desc');
|
||||
});
|
||||
|
||||
it('Should save journal entries that associate to the expense transaction.', async () => {
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
reference_no: 'ABC',
|
||||
payment_account_id: paymentAccount.id,
|
||||
description: 'desc',
|
||||
publish: 1,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 1000,
|
||||
description: 'category desc',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const transactions = await AccountTransaction.tenant().query()
|
||||
.where('reference_id', res.body.id)
|
||||
.where('reference_type', 'Expense');
|
||||
|
||||
const mappedTransactions = transactions.map(tr => ({
|
||||
...pick(tr, ['credit', 'debit', 'referenceId', 'referenceType']),
|
||||
}));
|
||||
|
||||
expect(mappedTransactions[0]).deep.equals({
|
||||
credit: 1000,
|
||||
debit: 0,
|
||||
referenceType: 'Expense',
|
||||
referenceId: res.body.id,
|
||||
});
|
||||
expect(mappedTransactions[1]).deep.equals({
|
||||
credit: 0,
|
||||
debit: 1000,
|
||||
referenceType: 'Expense',
|
||||
referenceId: res.body.id,
|
||||
});
|
||||
expect(transactions.length).equals(2);
|
||||
})
|
||||
});
|
||||
|
||||
describe('GET: `/expenses`', () => {
|
||||
it('Should response unauthorized if the user was not logged in.', async () => {
|
||||
const res = await request()
|
||||
.post('/api/expenses')
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should retrieve expenses with pagination meta.', async () => {
|
||||
await tenantFactory.create('expense');
|
||||
await tenantFactory.create('expense');
|
||||
|
||||
const res = await request()
|
||||
.get('/api/expenses')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.body.expenses).that.is.an('object');
|
||||
expect(res.body.expenses.results).that.is.an('array');
|
||||
});
|
||||
|
||||
it('Should retrieve expenses based on view roles conditions of the custom view.', () => {
|
||||
|
||||
});
|
||||
|
||||
it('Should sort expenses based on the given `column_sort_order` column on ASC direction.', () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe.only('DELETE: `/expenses/:id`', () => {
|
||||
it('Should response unauthorized if the user was not logged in.', async () => {
|
||||
const res = await request()
|
||||
.delete('/api/expenses')
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should response not found in case expense id was not exists on the storage.', async () => {
|
||||
const res = await request()
|
||||
.delete('/api/expenses/123321')
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'EXPENSE.NOT.FOUND', code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should delete the given expense transactions with associated categories.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
|
||||
const res = await request()
|
||||
.delete(`/api/expenses/${expense.id}`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(200);
|
||||
|
||||
const storedExpense = await Expense.tenant().query().where('id', expense.id);
|
||||
const storedExpenseCategories = await ExpenseCategory.tenant().query().where('expense_id', expense.id);
|
||||
|
||||
expect(storedExpense.length).equals(0);
|
||||
expect(storedExpenseCategories.length).equals(0);
|
||||
});
|
||||
|
||||
it.only('Should delete all journal entries that associated to the given expense.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
|
||||
const trans = { reference_id: expense.id, reference_type: 'Expense' };
|
||||
await tenantFactory.create('account_transaction', trans);
|
||||
await tenantFactory.create('account_transaction', trans);
|
||||
|
||||
const res = await request()
|
||||
.delete(`/api/expenses/${expense.id}`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
const foundTransactions = await AccountTransaction.tenant().query()
|
||||
.where('reference_type', 'Expense')
|
||||
.where('reference_id', expense.id);
|
||||
|
||||
expect(foundTransactions.length).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET: `/expenses/:id`', () => {
|
||||
it('Should response unauthorized if the user was not logged in.', async () => {
|
||||
const res = await request()
|
||||
.get('/api/expenses/123')
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should response not found in case the given expense id was not exists in the storage.', async () => {
|
||||
const res = await request()
|
||||
.get(`/api/expenses/321`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(404);
|
||||
});
|
||||
|
||||
it('Should retrieve expense metadata and associated expense categories.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseCategory = await tenantFactory.create('expense_category', {
|
||||
expense_id: expense.id,
|
||||
})
|
||||
const res = await request()
|
||||
.get(`/api/expenses/${expense.id}`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(200);
|
||||
|
||||
expect(res.body.expense.id).is.a('number');
|
||||
expect(res.body.expense.paymentAccountId).is.a('number');
|
||||
expect(res.body.expense.totalAmount).is.a('number');
|
||||
expect(res.body.expense.userId).is.a('number');
|
||||
expect(res.body.expense.referenceNo).is.a('string');
|
||||
expect(res.body.expense.description).is.a('string');
|
||||
expect(res.body.expense.categories).is.a('array');
|
||||
|
||||
expect(res.body.expense.categories[0].id).is.a('number');
|
||||
expect(res.body.expense.categories[0].description).is.a('string');
|
||||
expect(res.body.expense.categories[0].expenseAccountId).is.a('number');
|
||||
});
|
||||
|
||||
it('Should retrieve journal entries with expense metadata.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseCategory = await tenantFactory.create('expense_category', {
|
||||
expense_id: expense.id,
|
||||
});
|
||||
const trans = { reference_id: expense.id, reference_type: 'Expense' };
|
||||
await tenantFactory.create('account_transaction', trans);
|
||||
await tenantFactory.create('account_transaction', trans);
|
||||
|
||||
const res = await request()
|
||||
.get(`/api/expenses/${expense.id}`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.send();
|
||||
|
||||
expect(res.body.expense.journalEntries).is.an('array');
|
||||
expect(res.body.expense.journalEntries.length).equals(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST: `expenses/:id`', () => {
|
||||
it('Should response unauthorized in case the user was not logged in.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send();
|
||||
|
||||
expect(res.status).equals(401);
|
||||
expect(res.body.message).equals('Unauthorized');
|
||||
});
|
||||
|
||||
it('Should response the given expense id not exists on the storage.', async () => {
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post('/api/expenses/1233')
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
payment_account_id: 321,
|
||||
publish: true,
|
||||
categories: [
|
||||
{
|
||||
expense_account_id: expenseAccount.id,
|
||||
index: 1,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(res.status).equals(404);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'EXPENSE.NOT.FOUND', code: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response the given `payment_account_id` not exists.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
payment_account_id: 321,
|
||||
publish: true,
|
||||
categories: [
|
||||
{
|
||||
expense_account_id: expenseAccount.id,
|
||||
index: 1,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 400,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response the given `categories.*.expense_account_id` not exists.', async () => {
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expense = await tenantFactory.create('expense');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: 100,
|
||||
amount: 1000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'EXPENSE.ACCOUNTS.IDS.NOT.FOUND', code: 600, ids: [100],
|
||||
});
|
||||
});
|
||||
|
||||
it('Should response the total amount equals zero.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment().format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 0,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(res.status).equals(400);
|
||||
expect(res.body.errors).include.something.deep.equals({
|
||||
type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('Should update the expense transaction.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment('2009-01-02').format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
description: 'Updated description',
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 3000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(res.status).equals(200);
|
||||
|
||||
const updatedExpense = await Expense.tenant().query()
|
||||
.where('id', expense.id).first();
|
||||
|
||||
expect(updatedExpense.id).equals(expense.id);
|
||||
expect(updatedExpense.referenceNo).equals('123');
|
||||
expect(updatedExpense.description).equals('Updated description');
|
||||
expect(updatedExpense.totalAmount).equals(3000);
|
||||
expect(updatedExpense.paymentAccountId).equals(paymentAccount.id);
|
||||
});
|
||||
|
||||
it('Should delete the expense categories that associated to the expense transaction.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseCategory = await tenantFactory.create('expense_category', {
|
||||
expense_id: expense.id,
|
||||
});
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment('2009-01-02').format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
description: 'Updated description',
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 3000,
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundExpenseCategories = await ExpenseCategory.tenant()
|
||||
.query().where('id', expenseCategory.id)
|
||||
|
||||
expect(foundExpenseCategories.length).equals(0);
|
||||
});
|
||||
|
||||
it('Should insert the expense categories to associated to the expense transaction.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseCategory = await tenantFactory.create('expense_category', {
|
||||
expense_id: expense.id,
|
||||
});
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment('2009-01-02').format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
description: 'Updated description',
|
||||
categories: [
|
||||
{
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 3000,
|
||||
description: '__desc__',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundExpenseCategories = await ExpenseCategory.tenant()
|
||||
.query()
|
||||
.where('expense_id', expense.id)
|
||||
|
||||
expect(foundExpenseCategories.length).equals(1);
|
||||
expect(foundExpenseCategories[0].id).not.equals(expenseCategory.id);
|
||||
});
|
||||
|
||||
it('Should update the expense categories that associated to the expense transactions.', async () => {
|
||||
const expense = await tenantFactory.create('expense');
|
||||
const expenseCategory = await tenantFactory.create('expense_category', {
|
||||
expense_id: expense.id,
|
||||
});
|
||||
const paymentAccount = await tenantFactory.create('account');
|
||||
const expenseAccount = await tenantFactory.create('account');
|
||||
|
||||
const res = await request()
|
||||
.post(`/api/expenses/${expense.id}`)
|
||||
.set('x-access-token', loginRes.body.token)
|
||||
.set('organization-id', tenantWebsite.organizationId)
|
||||
.send({
|
||||
reference_no: '123',
|
||||
payment_date: moment('2009-01-02').format('YYYY-MM-DD'),
|
||||
payment_account_id: paymentAccount.id,
|
||||
publish: true,
|
||||
description: 'Updated description',
|
||||
categories: [
|
||||
{
|
||||
id: expenseCategory.id,
|
||||
index: 1,
|
||||
expense_account_id: expenseAccount.id,
|
||||
amount: 3000,
|
||||
description: '__desc__',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const foundExpenseCategory = await ExpenseCategory.tenant().query()
|
||||
.where('id', expenseCategory.id);
|
||||
|
||||
expect(foundExpenseCategory.length).equals(1);
|
||||
expect(foundExpenseCategory[0].expenseAccountId).equals(expenseAccount.id);
|
||||
expect(foundExpenseCategory[0].description).equals('__desc__');
|
||||
expect(foundExpenseCategory[0].amount).equals(3000);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user