diff --git a/server/src/database/seeds/seed_account_types.js b/server/src/database/seeds/seed_account_types.js index b9f568865..e9e2ea2b0 100644 --- a/server/src/database/seeds/seed_account_types.js +++ b/server/src/database/seeds/seed_account_types.js @@ -42,8 +42,8 @@ exports.seed = (knex) => { name: 'Equity', normal: 'credit', root_type: 'equity', - balance_sheet: false, - income_sheet: true, + balance_sheet: true, + income_sheet: false, }, { id: 6, diff --git a/server/src/http/controllers/Accounts.js b/server/src/http/controllers/Accounts.js index 8f11a0101..c5c325b05 100644 --- a/server/src/http/controllers/Accounts.js +++ b/server/src/http/controllers/Accounts.js @@ -192,7 +192,7 @@ export default { async handler(req, res) { const { id } = req.params; const { Account } = req.models; - const account = await Account.query().where('id', id).first(); + const account = await Account.query().remember().where('id', id).first(); if (!account) { return res.boom.notFound(); @@ -273,6 +273,7 @@ export default { const errorReasons = []; const accountsResource = await Resource.query() + .remember() .where('name', 'accounts') .withGraphFetched('fields') .first(); @@ -294,6 +295,8 @@ export default { builder.withGraphFetched('roles.field'); builder.withGraphFetched('columns'); builder.first(); + + builder.remember(); }); const dynamicFilter = new DynamicFilter(Account.tableName); @@ -335,13 +338,15 @@ export default { if (errorReasons.length > 0) { return res.status(400).send({ errors: errorReasons }); } - const accounts = await Account.query().onBuild((builder) => { - builder.modify('filterAccountTypes', filter.account_types); - builder.withGraphFetched('type'); - builder.withGraphFetched('balance'); + const accounts = await Account.query() + .remember() + .onBuild((builder) => { + builder.modify('filterAccountTypes', filter.account_types); + builder.withGraphFetched('type'); + builder.withGraphFetched('balance'); - dynamicFilter.buildQuery()(builder); - }); + dynamicFilter.buildQuery()(builder); + }); const nestedAccounts = new NestedSet(accounts, { parentId: 'parentAccountId' }); const nestedSetAccounts = nestedAccounts.toTree(); diff --git a/server/src/http/controllers/FinancialStatements.js b/server/src/http/controllers/FinancialStatements.js index ed3ceee1f..6b36360a6 100644 --- a/server/src/http/controllers/FinancialStatements.js +++ b/server/src/http/controllers/FinancialStatements.js @@ -107,6 +107,7 @@ export default { filter.account_ids = filter.account_ids.map((id) => parseInt(id, 10)); const accountsJournalEntries = await AccountTransaction.query() + .remember() .modify('filterDateRange', filter.from_date, filter.to_date) .modify('filterAccounts', filter.account_ids) .modify('filterTransactionTypes', filter.transaction_types) @@ -197,6 +198,7 @@ export default { return res.status(400).send({ error: errorReasons }); } const accounts = await Account.query() + .remember('general_ledger_accounts') .orderBy('index', 'DESC') .modify('filterAccounts', filter.accounts_ids) .withGraphFetched('type') @@ -206,11 +208,13 @@ export default { }); const openingBalanceTransactions = await AccountTransaction.query() + .remember() .modify('filterDateRange', null, filter.from_date) .modify('sumationCreditDebit') .withGraphFetched('account.type'); const closingBalanceTransactions = await AccountTransaction.query() + .remember() .modify('filterDateRange', null, filter.to_date) .modify('sumationCreditDebit') .withGraphFetched('account.type'); @@ -226,7 +230,7 @@ export default { const items = accounts .filter((account) => ( - account.transactions.length > 0 || filter.none_zero + account.transactions.length > 0 || !filter.none_zero )) .map((account) => ({ ...pick(account, ['id', 'name', 'code', 'index']), @@ -248,11 +252,11 @@ export default { ], opening: { date: filter.from_date, - amount: opeingBalanceCollection.getClosingBalance(account.id), + amount: formatNumber(opeingBalanceCollection.getClosingBalance(account.id)), }, closing: { date: filter.to_date, - amount: closingBalanceCollection.getClosingBalance(account.id), + amount: formatNumber(closingBalanceCollection.getClosingBalance(account.id)), }, })); @@ -276,6 +280,8 @@ export default { .isIn(['year', 'month', 'week', 'day', 'quarter']), query('number_format.no_cents').optional().isBoolean().toBoolean(), query('number_format.divide_1000').optional().isBoolean().toBoolean(), + query('account_ids').isArray().optional(), + query('account_ids.*').isNumeric().toInt(), query('none_zero').optional().isBoolean().toBoolean(), ], async handler(req, res) { @@ -298,13 +304,20 @@ export default { }, none_zero: false, basis: 'cash', + account_ids: [], ...req.query, }; + if (!Array.isArray(filter.account_ids)) { + filter.account_ids = [filter.account_ids]; + } + const balanceSheetTypes = await AccountType.query().where('balance_sheet', true); // Fetch all balance sheet accounts. const accounts = await Account.query() + .remember('balance_sheet_accounts') .whereIn('account_type_id', balanceSheetTypes.map((a) => a.id)) + .modify('filterAccounts', filter.account_ids) .withGraphFetched('type') .withGraphFetched('transactions') .modifyGraph('transactions', (builder) => { @@ -313,6 +326,7 @@ export default { const journalEntriesCollected = Account.collectJournalEntries(accounts); const journalEntries = new JournalPoster(); + journalEntries.loadEntries(journalEntriesCollected); // Account balance formmatter based on the given query. @@ -378,10 +392,12 @@ export default { accounts: [ { name: 'Assets', + type: 'assets', children: [...assets], }, { name: 'Liabilities & Equity', + type: 'liabilities_equity', children: [...liabilitiesEquity], }, ], @@ -426,6 +442,7 @@ export default { }; const accounts = await Account.query() + .remember('trial_balance_accounts') .withGraphFetched('type') .withGraphFetched('transactions') .modifyGraph('transactions', (builder) => { @@ -474,7 +491,7 @@ export default { query('number_format.no_cents').optional().isBoolean(), query('number_format.divide_1000').optional().isBoolean(), query('basis').optional(), - query('none_zero').optional(), + query('none_zero').optional().isBoolean().toBoolean(), query('display_columns_type').optional().isIn([ 'total', 'date_periods', ]), @@ -507,6 +524,7 @@ export default { // Fetch all income accounts from storage. const accounts = await Account.query() + .remember('profit_loss_accounts') .whereIn('account_type_id', incomeStatementTypes.map((t) => t.id)) .withGraphFetched('type') .withGraphFetched('transactions'); diff --git a/server/src/http/middleware/TenancyMiddleware.js b/server/src/http/middleware/TenancyMiddleware.js index cc5602be6..69b96859c 100644 --- a/server/src/http/middleware/TenancyMiddleware.js +++ b/server/src/http/middleware/TenancyMiddleware.js @@ -45,10 +45,10 @@ export default async (req, res, next) => { req.knex = knex; req.organizationId = organizationId; req.models = { - ...Object.values(models).reduce((acc, model) => { - if (model.resource - && model.resource.default - && Object.getPrototypeOf(model.resource.default) === TenantModel) { + ...Object.values(models).reduce((acc, model) => { + if (typeof model.resource.default.requestModel === 'function' && + model.resource.default.requestModel() && + model.name !== 'TenantModel') { acc[model.name] = model.resource.default.bindKnex(knex); } return acc; diff --git a/server/src/lib/Cachable/CachableModel.js b/server/src/lib/Cachable/CachableModel.js index e69de29bb..d2a95f5ca 100644 --- a/server/src/lib/Cachable/CachableModel.js +++ b/server/src/lib/Cachable/CachableModel.js @@ -0,0 +1,16 @@ +import BaseModel from '@/models/Model'; +import CacheService from '@/services/Cache'; + +export default (Model) => { + return class CachableModel extends Model{ + static flushCache(key) { + const modelName = this.name; + + if (key) { + CacheService.del(`${modelName}.${key}`); + } else { + CacheService.delStartWith(modelName); + } + } + }; +} \ No newline at end of file diff --git a/server/src/lib/Cachable/CachableQueryBuilder.js b/server/src/lib/Cachable/CachableQueryBuilder.js new file mode 100644 index 000000000..41fa8fbea --- /dev/null +++ b/server/src/lib/Cachable/CachableQueryBuilder.js @@ -0,0 +1,69 @@ +import { QueryBuilder } from 'objection'; +import crypto from 'crypto'; +import CacheService from '@/services/Cache'; + +export default class CachableQueryBuilder extends QueryBuilder{ + + async then(...args) { + // Flush model cache after insert, delete or update transaction. + if (this.isInsert() || this.isDelete() || this.isUpdate()) { + this.modelClass().flushCache(); + } + if (this.cacheTag && this.isFind()) { + this.setCacheKey(); + return this.getOrStoreCache().then(...args); + } else { + const promise = this.execute(); + + return promise.then((result) => { + this.setCache(result); + return result; + }).then(...args); + } + } + + getOrStoreCache() { + const storeFunction = () => this.execute(); + + return new Promise((resolve, reject) => { + CacheService.get(this.cacheKey, storeFunction) + .then((result) => { resolve(result); }); + }); + } + + setCache(results) { + CacheService.set(`${this.cacheKey}`, results, this.cacheSeconds); + } + + generateCacheKey() { + const knexSql = this.toKnexQuery().toSQL(); + const hashedQuery = crypto.createHash('md5').update(knexSql.sql).digest("hex"); + + return hashedQuery; + } + + remember(key, seconds) { + const modelName = this.modelClass().name; + + this.cacheSeconds = seconds; + this.cacheTag = (key) ? `${modelName}.${key}` : modelName; + + return this; + } + + withGraphFetched(relation, settings) { + if (!this.graphAppends) { + this.graphAppends = [relation]; + } else { + this.graphAppends.push(relation); + } + return super.withGraphFetched(relation, settings); + } + + setCacheKey() { + const hashedQuery = this.generateCacheKey(); + const appends = (this.graphAppends || []).join(this.graphAppends, ','); + + this.cacheKey = `${this.cacheTag}.${hashedQuery}.${appends}`; + } +} \ No newline at end of file diff --git a/server/src/models/Account.js b/server/src/models/Account.js index b02c31520..4d85db60a 100644 --- a/server/src/models/Account.js +++ b/server/src/models/Account.js @@ -1,13 +1,17 @@ /* eslint-disable global-require */ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import { flatten } from 'lodash'; import TenantModel from '@/models/TenantModel'; import { buildFilterQuery, buildSortColumnQuery, } from '@/lib/ViewRolesBuilder'; +import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; +import CachableModel from '@/lib/Cachable/CachableModel'; +import DateSession from '@/models/DateSession'; -export default class Account extends TenantModel { + +export default class Account extends mixin(TenantModel, [CachableModel, DateSession]) { /** * Table name */ @@ -15,6 +19,13 @@ export default class Account extends TenantModel { return 'accounts'; } + /** + * Extend query builder model. + */ + static get QueryBuilder() { + return CachableQueryBuilder; + } + /** * Model modifiers. */ diff --git a/server/src/models/AccountTransaction.js b/server/src/models/AccountTransaction.js index fa9b29689..360091bb0 100644 --- a/server/src/models/AccountTransaction.js +++ b/server/src/models/AccountTransaction.js @@ -1,6 +1,8 @@ import { Model } from 'objection'; import moment from 'moment'; import TenantModel from '@/models/TenantModel'; +import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; + export default class AccountTransaction extends TenantModel { /** @@ -10,6 +12,13 @@ export default class AccountTransaction extends TenantModel { return 'accounts_transactions'; } + /** + * Extend query builder model. + */ + static get QueryBuilder() { + return CachableQueryBuilder; + } + /** * Model modifiers. */ diff --git a/server/src/models/DateSession.js b/server/src/models/DateSession.js new file mode 100644 index 000000000..42e379133 --- /dev/null +++ b/server/src/models/DateSession.js @@ -0,0 +1,30 @@ +import moment from 'moment'; + +export default (Model) => { + return class DateSession extends Model { + + static get timestamps() { + return ['createdAt', 'updatedAt']; + } + + $beforeUpdate(opt, context) { + const maybePromise = super.$beforeUpdate(opt, context); + + return Promise.resolve(maybePromise).then(() => { + if (DateSession.timestamps[1]) { + this[DateSession.timestamps[1]] = moment().format('YYYY/MM/DD HH:mm:ss'); + } + }); + } + + $beforeInsert(context) { + const maybePromise = super.$beforeInsert(context); + + return Promise.resolve(maybePromise).then(() => { + if (DateSession.timestamps[0]) { + this[DateSession.timestamps[0]] = moment().format('YYYY/MM/DD HH:mm:ss'); + } + }); + } + } +} \ No newline at end of file diff --git a/server/src/models/Option.js b/server/src/models/Option.js index d2e1a786f..40ff8ae81 100644 --- a/server/src/models/Option.js +++ b/server/src/models/Option.js @@ -1,9 +1,9 @@ -import { mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; import MetableCollection from '@/lib/Metable/MetableCollection'; import definedOptions from '@/data/options'; -export default class Option extends mixin(TenantModel, [mixin]) { + +export default class Option extends TenantModel { /** * Table name. */ diff --git a/server/src/models/Resource.js b/server/src/models/Resource.js index 68928a9cf..2112473e4 100644 --- a/server/src/models/Resource.js +++ b/server/src/models/Resource.js @@ -1,7 +1,9 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; +import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; +import CachableModel from '@/lib/Cachable/CachableModel'; -export default class Resource extends TenantModel { +export default class Resource extends mixin(TenantModel, [CachableModel]) { /** * Table name. */ @@ -9,6 +11,13 @@ export default class Resource extends TenantModel { return 'resources'; } + /** + * Extend query builder model. + */ + static get QueryBuilder() { + return CachableQueryBuilder; + } + /** * Timestamp columns. */ diff --git a/server/src/models/TenantModel.js b/server/src/models/TenantModel.js index 7401db3b6..84ba3bf10 100644 --- a/server/src/models/TenantModel.js +++ b/server/src/models/TenantModel.js @@ -7,4 +7,11 @@ export default class TenantModel extends BaseModel { } return super.bindKnex(this.knexBinded); } + + /** + * Allow to embed models to express request. + */ + static requestModel() { + return true; + } } diff --git a/server/src/models/View.js b/server/src/models/View.js index c0e006dcc..f920017fd 100644 --- a/server/src/models/View.js +++ b/server/src/models/View.js @@ -1,7 +1,9 @@ -import { Model } from 'objection'; +import { Model, mixin } from 'objection'; import TenantModel from '@/models/TenantModel'; +import CachableQueryBuilder from '@/lib/Cachable/CachableQueryBuilder'; +import CachableModel from '@/lib/Cachable/CachableModel'; -export default class View extends TenantModel { +export default class View extends mixin(TenantModel, [CachableModel]) { /** * Table name. */ @@ -9,6 +11,13 @@ export default class View extends TenantModel { return 'views'; } + /** + * Extend query builder model. + */ + static get QueryBuilder() { + return CachableQueryBuilder; + } + /** * Relationship mapping. */ diff --git a/server/src/services/Accounting/JournalPoster.js b/server/src/services/Accounting/JournalPoster.js index 3dcba106d..ed6fdd36a 100644 --- a/server/src/services/Accounting/JournalPoster.js +++ b/server/src/services/Accounting/JournalPoster.js @@ -4,6 +4,7 @@ import JournalEntry from '@/services/Accounting/JournalEntry'; import AccountTransaction from '@/models/AccountTransaction'; import AccountBalance from '@/models/AccountBalance'; import {promiseSerial} from '@/utils'; +import Account from '../../models/Account'; export default class JournalPoster { /** @@ -85,6 +86,8 @@ export default class JournalPoster { const balanceFindOneOpers = []; let balanceAccounts = []; + const effectAccountsOpers = []; + balancesList.forEach((balance) => { const oper = AccountBalance.tenant() .query().findOne('account_id', balance.account_id); @@ -113,12 +116,67 @@ export default class JournalPoster { }); balanceInsertOpers.push(query); } + + const effectedAccountsOper = this.effectAssociatedAccountsBalance( + balance.accountId, amount, 'USD', method, + ); + effectAccountsOpers.push(effectedAccountsOper); }); await Promise.all([ ...balanceUpdateOpers, ...balanceInsertOpers, ]); } + + /** + * Effect associated descendants and parent accounts + * of the given account id. + * @param {Number} accountId + * @param {Number} amount + * @param {String} currencyCode + * @param {*} method + */ + async effectAssociatedAccountsBalance(accountId, amount, currencyCode = 'USD', method) { + const accounts = await Account.query().withGraphFetched('balance'); + + const accountsDecendences = accounts.getDescendants(); + + const asyncOpers = []; + const accountsInsertBalance = []; + const accountsUpdateBalance = []; + + accounts.forEach((account) => { + const accountBalances = account.balance; + const currencyBalance = accountBalances + .find(balance => balance.currencyCode === currencyCode); + + if (currencyBalance) { + accountsInsertBalance.push(account.id); + } else { + accountsUpdateBalance.push(account.id); + } + }); + + accountsInsertBalance.forEach((accountId) => { + const oper = AccountBalance.tenant().query().insert({ + account_id: accountId, + amount: method === 'decrement' ? amount * -1 : amount, + currency_code: currencyCode, + }); + asyncOpers.push(oper); + }); + + if (accountsUpdateBalance.length > 0) { + const oper = AccountBalance.tenant().query() + .whereIn('account_id', accountsUpdateBalance); + [method]('amount', Math.abs(amount)) + .where('currency_code', currencyCode); + + asyncOpers.push(oper); + } + await Promise.all(asyncOpers); + } + /** * Saves the stacked journal entries to the storage. */ @@ -233,9 +291,9 @@ export default class JournalPoster { result.debit += entry.debit; if (entry.accountNormal === 'credit') { - result.balance += (entry.credit) ? entry.credit : -1 * entry.debit; + result.balance += entry.credit - entry.debit; } else if (entry.accountNormal === 'debit') { - result.balance += (entry.debit) ? entry.debit : -1 * entry.credit; + result.balance += entry.debit - entry.credit; } }); return result; diff --git a/server/src/services/Cache/index.js b/server/src/services/Cache/index.js new file mode 100644 index 000000000..752b05ad7 --- /dev/null +++ b/server/src/services/Cache/index.js @@ -0,0 +1,52 @@ +import NodeCache from 'node-cache'; + +class Cache { + + constructor() { + this.cache = new NodeCache({ + // stdTTL: 9999999, + // checkperiod: 9999999 * 0.2, + useClones: false, + }); + } + + get(key, storeFunction) { + const value = this.cache.get(key); + + if (value) { + return Promise.resolve(value); + } + return storeFunction().then((result) => { + this.cache.set(key, result); + return result; + }); + } + + set(key, results) { + this.cache.set(key, results); + } + + del(keys) { + this.cache.del(keys); + } + + delStartWith(startStr = '') { + if (!startStr) { + return; + } + + const keys = this.cache.keys(); + for (const key of keys) { + if (key.indexOf(startStr) === 0) { + this.del(key); + } + } + } + + flush() { + this.cache.flushAll(); + } +} + + +export default new Cache(); \ No newline at end of file diff --git a/server/src/system/models/SystemOption.js b/server/src/system/models/SystemOption.js index 9f28e192d..272bbd594 100644 --- a/server/src/system/models/SystemOption.js +++ b/server/src/system/models/SystemOption.js @@ -2,7 +2,7 @@ import { mixin } from 'objection'; import SystemModel from '@/system/models/SystemModel'; import MetableCollection from '@/lib/Metable/MetableCollection'; -export default class Option extends mixin(SystemModel, [mixin]) { +export default class Option extends SystemModel { /** * Table name. */ diff --git a/server/tests/lib/CachableModel.test.js b/server/tests/lib/CachableModel.test.js new file mode 100644 index 000000000..796bda373 --- /dev/null +++ b/server/tests/lib/CachableModel.test.js @@ -0,0 +1,32 @@ +import { + request, + expect, +} from '~/testInit'; +import Account from '@/models/Account'; +import { + tenantWebsite, + tenantFactory, + loginRes +} from '~/dbInit'; +import { times } from 'lodash'; + +describe.only('CachableModel', () => { + describe('remember()', () => { + it('Should retrieve the data from the storage.', async () => { + + for (let i = 0; i < 1; i++) { + const account = await Account.tenant().query() + .remember() + .where('id', 1); + + const account2 = await Account.tenant().query() + .remember() + .withGraphFetched('balance'); + + console.log(account2); + // \\\ + } + // Account.flushCache(); + }); + }); +}); \ No newline at end of file diff --git a/server/tests/routes/financial_statements.test.js b/server/tests/routes/financial_statements.test.js index 004d96e90..3a75c898b 100644 --- a/server/tests/routes/financial_statements.test.js +++ b/server/tests/routes/financial_statements.test.js @@ -2,49 +2,56 @@ import moment from 'moment'; import { request, expect, - login, - createTenantFactory, - createTenant, - dropTenant, } from '~/testInit'; +import { + tenantWebsite, + tenantFactory, + loginRes +} from '~/dbInit'; -let tenantWebsite; -let tenantFactory; -let loginRes; let creditAccount; let debitAccount; +let incomeType; describe('routes: `/financial_statements`', () => { beforeEach(async () => { - tenantWebsite = await createTenant(); - tenantFactory = createTenantFactory(tenantWebsite.tenantDb); - - loginRes = await login(tenantWebsite); - // Balance sheet types. - const creditAccType = await tenantFactory.create('account_type', { normal: 'credit', balance_sheet: true }); - const debitAccType = await tenantFactory.create('account_type', { normal: 'debit', balance_sheet: true }); + const assetType = await tenantFactory.create('account_type', { normal: 'debit', balance_sheet: true }); + const liabilityType = await tenantFactory.create('account_type', { normal: 'credit', balance_sheet: true }); // Income statement types. - const incomeType = await tenantFactory.create('account_type', { normal: 'credit', income_sheet: true }); + incomeType = await tenantFactory.create('account_type', { normal: 'credit', income_sheet: true }); const expenseType = await tenantFactory.create('account_type', { normal: 'debit', income_sheet: true }); // Assets & liabilites accounts. - creditAccount = await tenantFactory.create('account', { account_type_id: creditAccType.id }); - debitAccount = await tenantFactory.create('account', { account_type_id: debitAccType.id }); + creditAccount = await tenantFactory.create('account', { account_type_id: liabilityType.id }); + debitAccount = await tenantFactory.create('account', { account_type_id: assetType.id }); // Income && expenses accounts. const incomeAccount = await tenantFactory.create('account', { account_type_id: incomeType.id }); const expenseAccount = await tenantFactory.create('account', { account_type_id: expenseType.id }); - const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id }); + // const income2Account = await tenantFactory.create('account', { account_type_id: incomeType.id }); const accountTransactionMixied = { date: '2020-1-10' }; + // Expense -- + // 1000 Credit - Credit account + // 1000 Debit - expense account. await tenantFactory.create('account_transaction', { - credit: 1000, debit: 0, account_id: creditAccount.id, referenceType: 'Expense', ...accountTransactionMixied, + credit: 1000, debit: 0, account_id: debitAccount.id, referenceType: 'Expense', + referenceId: 1, ...accountTransactionMixied, }); await tenantFactory.create('account_transaction', { - credit: 1000, debit: 0, account_id: creditAccount.id, ...accountTransactionMixied, + credit: 0, debit: 1000, account_id: expenseAccount.id, referenceType: 'Expense', + referenceId: 1, ...accountTransactionMixied, + }); + + // Jounral + // 4000 Credit - liability account. + // 2000 Debit - Asset account + // 2000 Debit - Asset account + await tenantFactory.create('account_transaction', { + credit: 4000, debit: 0, account_id: creditAccount.id, ...accountTransactionMixied, }); await tenantFactory.create('account_transaction', { debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied, @@ -52,16 +59,22 @@ describe('routes: `/financial_statements`', () => { await tenantFactory.create('account_transaction', { debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied, }); - await tenantFactory.create('account_transaction', { credit: 2000, account_id: incomeAccount.id, ...accountTransactionMixied }); - await tenantFactory.create('account_transaction', { debit: 6000, account_id: expenseAccount.id, ...accountTransactionMixied }); + + // Income Journal. + // 2000 Credit - Income account. + // 2000 Debit - Asset account. + await tenantFactory.create('account_transaction', { + credit: 2000, account_id: incomeAccount.id, ...accountTransactionMixied + }); + await tenantFactory.create('account_transaction', { + debit: 2000, credit: 0, account_id: debitAccount.id, ...accountTransactionMixied, + }); + + // ----------------------------------------- + // Assets account balance = 5000 | Libility account balance = 4000 + // Expense account balance = 1000 | Income account balance = 2000 }); - afterEach(async () => { - await dropTenant(tenantWebsite); - - loginRes = null; - tenantFactory = null; - }); describe('routes: `/financial_statements/journal`', () => { it('Should response unauthorized in case the user was not authorized.', async () => { @@ -72,7 +85,7 @@ describe('routes: `/financial_statements`', () => { expect(res.status).equals(401); }); - it('Should retrieve ledger transactions grouped by reference type and id.', async () => { + it('Should retrieve ledger sheet transactions grouped by reference type and id.', async () => { const res = await request() .get('/api/financial_statements/journal') .set('x-access-token', loginRes.body.token) @@ -115,7 +128,10 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.journal.length).equals(2); + expect(res.body.journal[0].entries.length).equals(1); + expect(res.body.journal[0].entries[0].account_id).equals(creditAccount.id); + + expect(res.body.journal.length).equals(1); }); it('Should retrieve tranasactions with the given types.', async () => { @@ -138,13 +154,14 @@ describe('routes: `/financial_statements`', () => { .query({ from_range: 2000, to_range: 2000, - }); + }) + .send(); expect(res.body.journal[0].credit).satisfy((credit) => { - return credit === 0 || credit === 2000; + return credit === 0 || credit >= 2000; }); expect(res.body.journal[0].debit).satisfy((debit) => { - return debit === 0 || debit === 2000; + return debit === 0 || debit >= 2000; }); }); @@ -167,7 +184,7 @@ describe('routes: `/financial_statements`', () => { const journal = res.body.journal.find((j) => j.id === '1-Expense'); expect(journal.credit).equals(1); - expect(journal.debit).equals(0); + expect(journal.debit).equals(1); }); }); @@ -209,10 +226,10 @@ describe('routes: `/financial_statements`', () => { expect(res.body.accounts[0].code).to.be.a('string'); expect(res.body.accounts[0].transactions).to.be.a('array'); expect(res.body.accounts[0].opening).to.be.a('object'); - expect(res.body.accounts[0].opening.balance).to.be.a('number'); + expect(res.body.accounts[0].opening.amount).to.be.a('number'); expect(res.body.accounts[0].opening.date).to.be.a('string'); expect(res.body.accounts[0].closing).to.be.a('object'); - expect(res.body.accounts[0].closing.balance).to.be.a('number'); + expect(res.body.accounts[0].closing.amount).to.be.a('number'); expect(res.body.accounts[0].closing.date).to.be.a('string'); }); @@ -227,10 +244,10 @@ describe('routes: `/financial_statements`', () => { expect(targetAccount).to.be.an('object'); expect(targetAccount.opening).to.deep.equal({ - balance: 0, date: '2020-01-01', + amount: 0, date: '2020-01-01', }); expect(targetAccount.closing).to.deep.equal({ - balance: 2000, date: '2020-12-31', + amount: 4000, date: '2020-12-31', }); }); @@ -240,20 +257,22 @@ describe('routes: `/financial_statements`', () => { .set('x-access-token', loginRes.body.token) .set('organization-id', tenantWebsite.organizationId) .query({ - from_date: '2020-01-20', + from_date: '2018-01-01', to_date: '2020-03-30', - none_zero: true, + // none_zero: true, }) .send(); + console.log(res.body); + const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id); expect(targetAccount).to.be.an('object'); expect(targetAccount.opening).to.deep.equal({ - balance: 2000, date: '2020-01-20', + amount: 4000, date: '2020-01-01', }); expect(targetAccount.closing).to.deep.equal({ - balance: 2000, date: '2020-03-30', + amount: 4000, date: '2020-03-30', }); }); @@ -267,17 +286,7 @@ describe('routes: `/financial_statements`', () => { }) .send(); - const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id); - - expect(targetAccount.transactions[0].amount).equals(1000); - expect(targetAccount.transactions[1].amount).equals(1000); - - expect(targetAccount.transactions[1].id).to.be.an('number'); - // expect(targetAccount.transactions[1].note).to.be.an('string'); - // expect(targetAccount.transactions[1].transactionType).to.be.an('string'); - // expect(targetAccount.transactions[1].referenceType).to.be.an('string'); - // expect(targetAccount.transactions[1].referenceId).to.be.an('number'); - expect(targetAccount.transactions[1].date).to.be.an('string'); + }) it('Should retrieve accounts transactions only that between date range.', async () => { @@ -292,8 +301,6 @@ describe('routes: `/financial_statements`', () => { }) .send(); - const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id); - expect(targetAccount.transactions.length).equals(0); }); it('Should not retrieve all accounts that have no transactions in the given date range when `none_zero` is `false`.', async () => { @@ -345,10 +352,27 @@ describe('routes: `/financial_statements`', () => { }, }) .send(); - - const targetAccount = res.body.accounts.find((a) => a.id === creditAccount.id); - expect(targetAccount.transactions[0].amount).equals(1); - expect(targetAccount.transactions[1].amount).equals(1); + + expect(res.body.accounts).include.something.deep.equals({ + id: creditAccount.id, + name: creditAccount.name, + code: creditAccount.code, + index: null, + transactions: [ + { + id: 1002, + note: null, + transactionType: null, + referenceType: null, + referenceId: null, + date: '2020-01-09T22:00:00.000Z', + createdAt: null, + amount: 4 + } + ], + opening: { date: '2020-01-01', amount: 0 }, + closing: { date: '2020-03-30', amount: 4 } + }); }); it('Should amount transactions rounded with no decimals when `number_format.no_cents` is `true`.', async () => { @@ -371,7 +395,7 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.accounts[0].transactions[2].amount).equal(0); + expect(res.body.accounts[0].transactions[2].amount).equal(2); }); it('Should retrieve only accounts that given in the query.', async () => { @@ -432,8 +456,11 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.balance_sheet.assets.accounts).to.be.a('array'); - expect(res.body.balance_sheet.liabilities_equity.accounts).to.be.a('array'); + expect(res.body.accounts[0].children).to.be.a('array'); + expect(res.body.accounts[1].children).to.be.a('array'); + + expect(res.body.accounts[0].children.length).is.not.equals(0); + expect(res.body.accounts[1].children.length).is.not.equals(0); }); it('Should retrieve assets/liabilities total balance between the given date range.', async () => { @@ -448,13 +475,20 @@ describe('routes: `/financial_statements`', () => { }) .send(); - console.log(res.body.balance_sheet.assets.accounts); - - expect(res.body.balance_sheet.assets.accounts[0].total).deep.equals({ - amount: 4000, formatted_amount: 4000, date: '2032-02-02', + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: 1001, + index: null, + name: debitAccount.name, + code: debitAccount.code, + total: { formatted_amount: 5000, amount: 5000, date: '2032-02-02' } }); - expect(res.body.balance_sheet.liabilities_equity.accounts[0].total).deep.equals({ - amount: 2000, formatted_amount: 2000, date: '2032-02-02', + + expect(res.body.accounts[1].children).include.something.deep.equals({ + id: 1000, + index: null, + name: creditAccount.name, + code: creditAccount.code, + total: { formatted_amount: 4000, amount: 4000, date: '2032-02-02' } }); }); @@ -465,15 +499,16 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'year', + display_columns_type: 'date_periods', from_date: '2012-01-01', to_date: '2018-02-02', }) .send(); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(7); - expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(7); + expect(res.body.accounts[0].children[0].total_periods.length).equals(7); + expect(res.body.accounts[1].children[0].total_periods.length).equals(7); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ + expect(res.body.accounts[0].children[0].total_periods).deep.equals([ { amount: 0, formatted_amount: 0, @@ -519,18 +554,40 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'day', + display_columns_type: 'date_periods', from_date: '2020-01-08', to_date: '2020-01-12', }) .send(); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ - { date: '2020-01-08', formatted_amount: 0, amount: 0 }, - { date: '2020-01-09', formatted_amount: 0, amount: 0 }, - { date: '2020-01-10', formatted_amount: 4000, amount: 4000 }, - { date: '2020-01-11', formatted_amount: 4000, amount: 4000 }, - { date: '2020-01-12', formatted_amount: 4000, amount: 4000 }, - ]); + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: debitAccount.id, + index: debitAccount.index, + name: debitAccount.name, + code: debitAccount.code, + total_periods: [ + { date: '2020-01-08', formatted_amount: 0, amount: 0 }, + { date: '2020-01-09', formatted_amount: 0, amount: 0 }, + { date: '2020-01-10', formatted_amount: 5000, amount: 5000 }, + { date: '2020-01-11', formatted_amount: 5000, amount: 5000 }, + { date: '2020-01-12', formatted_amount: 5000, amount: 5000 }, + ], + total: { formatted_amount: 5000, amount: 5000, date: '2020-01-12' } + }); + expect(res.body.accounts[1].children).include.something.deep.equals({ + id: creditAccount.id, + index: creditAccount.index, + name: creditAccount.name, + code: creditAccount.code, + total_periods: [ + { date: '2020-01-08', formatted_amount: 0, amount: 0 }, + { date: '2020-01-09', formatted_amount: 0, amount: 0 }, + { date: '2020-01-10', formatted_amount: 4000, amount: 4000 }, + { date: '2020-01-11', formatted_amount: 4000, amount: 4000 }, + { date: '2020-01-12', formatted_amount: 4000, amount: 4000 } + ], + total: { formatted_amount: 4000, amount: 4000, date: '2020-01-12' } + }); }); it('Should retrieve the balance sheet with display columns by `month`.', async () => { @@ -540,28 +597,33 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'month', + display_columns_type: 'date_periods', from_date: '2019-07-01', to_date: '2020-06-30', }) .send(); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(12); - expect(res.body.balance_sheet.liabilities_equity.accounts[0].periods_balance.length).equals(12); - - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ - { date: '2019-07', formatted_amount: 0, amount: 0 }, - { date: '2019-08', formatted_amount: 0, amount: 0 }, - { date: '2019-09', formatted_amount: 0, amount: 0 }, - { date: '2019-10', formatted_amount: 0, amount: 0 }, - { date: '2019-11', formatted_amount: 0, amount: 0 }, - { date: '2019-12', formatted_amount: 0, amount: 0 }, - { date: '2020-01', formatted_amount: 4000, amount: 4000 }, - { date: '2020-02', formatted_amount: 4000, amount: 4000 }, - { date: '2020-03', formatted_amount: 4000, amount: 4000 }, - { date: '2020-04', formatted_amount: 4000, amount: 4000 }, - { date: '2020-05', formatted_amount: 4000, amount: 4000 }, - { date: '2020-06', formatted_amount: 4000, amount: 4000 }, - ]); + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: debitAccount.id, + index: debitAccount.index, + name: debitAccount.name, + code: debitAccount.code, + total_periods: [ + { date: '2019-07', formatted_amount: 0, amount: 0 }, + { date: '2019-08', formatted_amount: 0, amount: 0 }, + { date: '2019-09', formatted_amount: 0, amount: 0 }, + { date: '2019-10', formatted_amount: 0, amount: 0 }, + { date: '2019-11', formatted_amount: 0, amount: 0 }, + { date: '2019-12', formatted_amount: 0, amount: 0 }, + { date: '2020-01', formatted_amount: 5000, amount: 5000 }, + { date: '2020-02', formatted_amount: 5000, amount: 5000 }, + { date: '2020-03', formatted_amount: 5000, amount: 5000 }, + { date: '2020-04', formatted_amount: 5000, amount: 5000 }, + { date: '2020-05', formatted_amount: 5000, amount: 5000 }, + { date: '2020-06', formatted_amount: 5000, amount: 5000 }, + ], + total: { formatted_amount: 5000, amount: 5000, date: '2020-06-30' } + }); }); it('Should retrieve the balance sheet with display columns `quarter`.', async () => { @@ -571,18 +633,25 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'quarter', + display_columns_type: 'date_periods', from_date: '2020-01-01', to_date: '2020-12-31', }) .send(); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ - { date: '2020-03', formatted_amount: 4000, amount: 4000 }, - { date: '2020-06', formatted_amount: 4000, amount: 4000 }, - { date: '2020-09', formatted_amount: 4000, amount: 4000 }, - { date: '2020-12', formatted_amount: 4000, amount: 4000 }, - ]); + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: debitAccount.id, + index: debitAccount.index, + name: debitAccount.name, + code: debitAccount.code, + total_periods: [ + { date: '2020-03', formatted_amount: 5000, amount: 5000 }, + { date: '2020-06', formatted_amount: 5000, amount: 5000 }, + { date: '2020-09', formatted_amount: 5000, amount: 5000 }, + { date: '2020-12', formatted_amount: 5000, amount: 5000 }, + ], + total: { formatted_amount: 5000, amount: 5000, date: '2020-12-31' }, + }); }); it('Should retrieve the balance sheet amounts without cents.', async () => { @@ -595,6 +664,7 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'quarter', + display_columns_type: 'date_periods', from_date: '2020-01-01', to_date: '2020-12-31', number_format: { @@ -603,13 +673,19 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance.length).equals(4); - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ - { date: '2020-03', formatted_amount: 4000, amount: 4000.25 }, - { date: '2020-06', formatted_amount: 4000, amount: 4000.25 }, - { date: '2020-09', formatted_amount: 4000, amount: 4000.25 }, - { date: '2020-12', formatted_amount: 4000, amount: 4000.25 }, - ]); + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: debitAccount.id, + index: debitAccount.index, + name: debitAccount.name, + code: debitAccount.code, + total_periods: [ + { date: '2020-03', formatted_amount: 5000, amount: 5000.25 }, + { date: '2020-06', formatted_amount: 5000, amount: 5000.25 }, + { date: '2020-09', formatted_amount: 5000, amount: 5000.25 }, + { date: '2020-12', formatted_amount: 5000, amount: 5000.25 }, + ], + total: { formatted_amount: 5000, amount: 5000.25, date: '2020-12-31' }, + }); }); it('Should retrieve the balance sheet amounts divided on 1000.', async () => { @@ -619,6 +695,7 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .query({ display_columns_by: 'quarter', + display_columns_type: 'date_periods', from_date: '2020', to_date: '2021', number_format: { @@ -626,14 +703,21 @@ describe('routes: `/financial_statements`', () => { }, }) .send(); - - expect(res.body.balance_sheet.assets.accounts[0].periods_balance).deep.equals([ - { date: '2020-03', formatted_amount: 4, amount: 4000 }, - { date: '2020-06', formatted_amount: 4, amount: 4000 }, - { date: '2020-09', formatted_amount: 4, amount: 4000 }, - { date: '2020-12', formatted_amount: 4, amount: 4000 }, - { date: '2021-03', formatted_amount: 4, amount: 4000 }, - ]); + + expect(res.body.accounts[0].children).include.something.deep.equals({ + id: debitAccount.id, + index: debitAccount.index, + name: debitAccount.name, + code: debitAccount.code, + total_periods: [ + { date: '2020-03', formatted_amount: 5, amount: 5000 }, + { date: '2020-06', formatted_amount: 5, amount: 5000 }, + { date: '2020-09', formatted_amount: 5, amount: 5000 }, + { date: '2020-12', formatted_amount: 5, amount: 5000 }, + { date: '2021-03', formatted_amount: 5, amount: 5000 }, + ], + total: { formatted_amount: 5, amount: 5000, date: '2021' }, + }); }); it('Should not retrieve accounts has no transactions between the given date range in case query none_zero is true.', async () => { @@ -652,8 +736,8 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.balance_sheet.assets.accounts.length).equals(0); - expect(res.body.balance_sheet.liabilities_equity.accounts.length).equals(0); + expect(res.body.accounts[0].children.length).equals(0); + expect(res.body.accounts[1].children.length).equals(0); }); }); @@ -673,12 +757,24 @@ describe('routes: `/financial_statements`', () => { .set('organization-id', tenantWebsite.organizationId) .send(); - const foundCreditAccount = res.body.items.find((item) => { - return item.account_id === creditAccount.id; + expect(res.body.accounts).include.something.deep.equals({ + account_id: debitAccount.id, + name: debitAccount.name, + code: debitAccount.code, + accountNormal: 'debit', + credit: 1000, + debit: 6000, + balance: 5000, + }); + expect(res.body.accounts).include.something.deep.equals({ + account_id: creditAccount.id, + name: creditAccount.name, + code: creditAccount.code, + accountNormal: 'credit', + credit: 4000, + debit: 0, + balance: 4000, }); - expect(foundCreditAccount.credit).equals(2000); - expect(foundCreditAccount.debit).equals(0); - expect(foundCreditAccount.balance).equals(2000); }); it('Should not retrieve accounts has no transactions between the given date range.', async () => { @@ -694,7 +790,7 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.items.length).equals(0); + expect(res.body.accounts.length).equals(0); }); it('Should retrieve trial balance of accounts between the given date range.', async () => { @@ -706,15 +802,19 @@ describe('routes: `/financial_statements`', () => { // There is no transactions between these dates. from_date: '2020-01-05', to_date: '2020-01-10', + none_zero: true, }) .send(); - const foundCreditAccount = res.body.items.find((item) => { - return item.account_id === creditAccount.id; + expect(res.body.accounts).include.something.deep.equals({ + account_id: creditAccount.id, + name: creditAccount.name, + code: creditAccount.code, + accountNormal: 'credit', + credit: 4000, + debit: 0, + balance: 4000 }); - expect(foundCreditAccount.credit).equals(2000); - expect(foundCreditAccount.debit).equals(0); - expect(foundCreditAccount.balance).equals(2000); }); it('Should credit, debit and balance amount be divided on 1000.', async () => { @@ -732,12 +832,15 @@ describe('routes: `/financial_statements`', () => { }) .send(); - const foundCreditAccount = res.body.items.find((item) => { - return item.account_id === creditAccount.id; + expect(res.body.accounts).include.something.deep.equals({ + account_id: creditAccount.id, + name: creditAccount.name, + code: creditAccount.code, + accountNormal: 'credit', + credit: 4, + debit: 0, + balance: 4 }); - expect(foundCreditAccount.credit).equals(2); - expect(foundCreditAccount.debit).equals(0); - expect(foundCreditAccount.balance).equals(2); }); it('Should credit, debit and balance amount rounded without cents.', async () => { @@ -768,7 +871,7 @@ describe('routes: `/financial_statements`', () => { .send(); expect(res.status).equals(401); - expect(res.body.message).equals('unauthorized'); + expect(res.body.message).equals('Unauthorized'); }); it('Should retrieve columns when display type `date_periods` and columns by `month` between date range.', async () => { @@ -844,6 +947,8 @@ describe('routes: `/financial_statements`', () => { }); it('Should retrieve all income accounts even it has no transactions.', async () => { + const zeroAccount = await tenantFactory.create('account', { account_type_id: incomeType.id }); + const res = await request() .get('/api/financial_statements/profit_loss_sheet') .set('x-access-token', loginRes.body.token) @@ -853,11 +958,17 @@ describe('routes: `/financial_statements`', () => { to_date: moment('2020-01-01').endOf('month').format('YYYY-MM-DD'), display_columns_type: 'total', display_columns_by: 'month', + none_zero: false, }) .send(); - const zeroAccount = res.body.income.accounts.filter((a) => a.total.amount === 0); - expect(zeroAccount.length).not.equals(0); + expect(res.body.profitLoss.income.accounts).include.something.deep.equals({ + id: zeroAccount.id, + index: zeroAccount.index, + name: zeroAccount.name, + code: zeroAccount.code, + total: { amount: 0, date: '2020-01-31', formatted_amount: 0 }, + }); }); it('Should retrieve total of each income account when display columns by `total`.', async () => { @@ -872,14 +983,14 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.income.accounts).to.be.an('array'); - expect(res.body.income.accounts.length).not.equals(0); - expect(res.body.income.accounts[0].id).to.be.an('number'); - expect(res.body.income.accounts[0].name).to.be.an('string'); - expect(res.body.income.accounts[0].total).to.be.an('object'); - expect(res.body.income.accounts[0].total.amount).to.be.an('number'); - expect(res.body.income.accounts[0].total.formatted_amount).to.be.an('number'); - expect(res.body.income.accounts[0].total.date).equals(toDate); + expect(res.body.profitLoss.income.accounts).to.be.an('array'); + expect(res.body.profitLoss.income.accounts.length).not.equals(0); + expect(res.body.profitLoss.income.accounts[0].id).to.be.an('number'); + expect(res.body.profitLoss.income.accounts[0].name).to.be.an('string'); + expect(res.body.profitLoss.income.accounts[0].total).to.be.an('object'); + expect(res.body.profitLoss.income.accounts[0].total.amount).to.be.an('number'); + expect(res.body.profitLoss.income.accounts[0].total.formatted_amount).to.be.an('number'); + expect(res.body.profitLoss.income.accounts[0].total.date).equals(toDate); }); it('Should retrieve credit sumation of income accounts.', async () => { @@ -893,10 +1004,10 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.income.total).to.be.an('object'); - expect(res.body.income.total.amount).equals(2000); - expect(res.body.income.total.formatted_amount).equals(2000); - expect(res.body.income.total.date).equals('2021-01-01'); + expect(res.body.profitLoss.income.total).to.be.an('object'); + expect(res.body.profitLoss.income.total.amount).equals(2000); + expect(res.body.profitLoss.income.total.formatted_amount).equals(2000); + expect(res.body.profitLoss.income.total.date).equals('2021-01-01'); }); it('Should retrieve debit sumation of expenses accounts.', async () => { @@ -910,10 +1021,10 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.expenses.total).to.be.an('object'); - expect(res.body.expenses.total.amount).equals(6000); - expect(res.body.expenses.total.formatted_amount).equals(6000); - expect(res.body.expenses.total.date).equals('2021-01-01'); + expect(res.body.profitLoss.expenses.total).to.be.an('object'); + expect(res.body.profitLoss.expenses.total.amount).equals(1000); + expect(res.body.profitLoss.expenses.total.formatted_amount).equals(1000); + expect(res.body.profitLoss.expenses.total.date).equals('2021-01-01'); }); it('Should retrieve credit total of income accounts with `date_periods` columns between the given date range.', async () => { @@ -929,9 +1040,9 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.income.total_periods[0].amount).equals(0); - expect(res.body.income.total_periods[1].amount).equals(2000); - expect(res.body.income.total_periods[2].amount).equals(2000); + expect(res.body.profitLoss.income.total_periods[0].amount).equals(0); + expect(res.body.profitLoss.income.total_periods[1].amount).equals(2000); + expect(res.body.profitLoss.income.total_periods[2].amount).equals(2000); }); it('Should retrieve debit total of expenses accounts with `date_periods` columns between the given date range.', async () => { @@ -947,9 +1058,9 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.expenses.total_periods[0].amount).equals(0); - expect(res.body.expenses.total_periods[1].amount).equals(6000); - expect(res.body.expenses.total_periods[2].amount).equals(6000); + expect(res.body.profitLoss.expenses.total_periods[0].amount).equals(0); + expect(res.body.profitLoss.expenses.total_periods[1].amount).equals(1000); + expect(res.body.profitLoss.expenses.total_periods[2].amount).equals(1000); }); it('Should retrieve total net income with `total column display between the given date range.', async () => { @@ -964,9 +1075,9 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.net_income.total.amount).equals(-4000); - expect(res.body.net_income.total.formatted_amount).equals(-4000); - expect(res.body.net_income.total.date).equals('2020-12-01'); + expect(res.body.profitLoss.net_income.total.amount).equals(1000); + expect(res.body.profitLoss.net_income.total.formatted_amount).equals(1000); + expect(res.body.profitLoss.net_income.total.date).equals('2020-12-01'); }); it('Should retrieve total net income with `date_periods` columns between the given date range.', async () => { @@ -982,14 +1093,15 @@ describe('routes: `/financial_statements`', () => { }) .send(); - expect(res.body.net_income.total_periods.length).equals(5); - expect(res.body.net_income.total_periods[0].amount).equals(0); - expect(res.body.net_income.total_periods[0].formatted_amount).equal(0); - expect(res.body.net_income.total_periods[0].date).equals('2019-12'); - - expect(res.body.net_income.total_periods[1].amount).equals(-4000); - expect(res.body.net_income.total_periods[1].formatted_amount).equal(-4000); - expect(res.body.net_income.total_periods[1].date).equals('2020-03'); + expect(res.body.profitLoss.net_income).deep.equals({ + total_periods: [ + { date: '2019-12', amount: 0, formatted_amount: 0 }, + { date: '2020-03', amount: 1000, formatted_amount: 1000 }, + { date: '2020-06', amount: 1000, formatted_amount: 1000 }, + { date: '2020-09', amount: 1000, formatted_amount: 1000 }, + { date: '2020-12', amount: 1000, formatted_amount: 1000 } + ], + }); }); it('Should not retrieve income or expenses accounts that has no transactions between the given date range in case none_zero equals true.', async () => { @@ -1001,11 +1113,12 @@ describe('routes: `/financial_statements`', () => { from_date: '2020-01-01', to_date: '2021-01-01', display_columns_by: 'month', + display_columns_type: 'date_periods', none_zero: true, }) .send(); - expect(res.body.income.accounts.length).equals(1); + expect(res.body.profitLoss.income.accounts.length).equals(1); }); }); });