Skip to content

Commit

Permalink
refactor(Trial Balance): combine TB APIs into one
Browse files Browse the repository at this point in the history
BREAKING CHANGE The Trial Balance APIs have now been combined into a
single route.

By combining routes, we simplify the number of HTTP requests sent for the
Trial Balance information, and allows the client to maintain stateful
information about the errors as well as the summary information.  It
further simplifies the parsing of information sent from the client -
everything is contained in a temporary table shared in the database
transaction.
  • Loading branch information
jniles committed Sep 25, 2017
1 parent f82f8ec commit b6931a9
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 130 deletions.
11 changes: 3 additions & 8 deletions server/config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const financialPatient = require('../controllers/finance/patient');
const dashboardDebtors = require('../controllers/dashboard/debtorGroups');
const stats = require('../controllers/dashboard/stats');

//looking up an entity by it reference
// looking up an entity by it reference
const refenceLookup = require('../lib/referenceLookup');

// expose routes to the server.
Expand Down Expand Up @@ -180,17 +180,14 @@ exports.configure = function configure(app) {
app.post('/journal/:record_uuid/edit', journal.editTransaction);
app.post('/journal/:uuid/reverse', journal.reverse);



// API for general ledger
app.get('/general_ledger', generalLedger.list);
app.get('/general_ledger/accounts', generalLedger.listAccounts);
app.put('/general_ledger/comments', generalLedger.commentAccountStatement);

// API for trial balance
app.post('/trial_balance/data_per_account', trialBalance.getDataPerAccount);
app.post('/trial_balance/checks', trialBalance.checkTransactions);
app.post('/trial_balance/post_transactions', trialBalance.postToGeneralLedger);
app.post('/journal/trialbalance', trialBalance.runTrialBalance);
app.post('/journal/transactions', trialBalance.postToGeneralLedger);

/* fiscal year controller */
app.get('/fiscal', fiscal.list);
Expand All @@ -207,15 +204,13 @@ exports.configure = function configure(app) {
// Period routes
app.get('/periods', fiscalPeriod.list);


/* load a user's tree */
app.get('/tree', tree.generate);

// snis controller
app.get('/snis/healthZones', snis.healthZones);

// Employee management
app.get('/employee_list/', employees.list);
app.get('/holiday_list/:pp/:employee_id', employees.listHolidays);
app.get('/getCheckHollyday/', employees.checkHoliday);
app.get('/getCheckOffday/', employees.checkOffday);
Expand Down
134 changes: 47 additions & 87 deletions server/controllers/finance/trialBalance/index.js
Original file line number Diff line number Diff line change
@@ -1,105 +1,65 @@
/**
* The trial balance provides a description of what the general
* ledger would look like after posting data from the
* posting journal to the general ledger.
* It also submit errors back to the client.
* @overview trialbalance
*
* @description
* This module contains the HTTP wrappers for the Trial Balance. Most of the SQL
* has been migrated to stored procedures in MySQL with lengthy descriptions of each
* in comments above the respective methods.
*
* @requires db
* @requires BadRequest
*/

const db = require('../../../lib/db');
const BadRequest = require('../../../lib/errors/BadRequest');

exports.getDataPerAccount = function getDataPerAccount(req, res, next) {
// wrapper to reuse staging code
// txn is a db.transaction()
function stageTrialBalanceTransactions(txn, transactions) {
transactions.forEach((transaction) => {
txn.addQuery('CALL StageTrialBalanceTransaction(?);', db.bid(transaction));
});
}

exports.runTrialBalance = function runTrialBalance(req, res, next) {
const transactions = req.body.transactions;
const hasInvalidTransactions = !(transactions && Array.isArray(transactions) && transactions.length);

if (!transactions) {
next(new BadRequest('The transaction list is null or undefined'));
return;
if (hasInvalidTransactions) {
return next(new BadRequest(
'No transactions were submitted. Please ensure that some are selected.',
'POSTING_JOURNAL.ERRORS.MISSING_TRANSACTIONS'
));
}

// This is a complicated query, but it performs correctly.
// 1) The beginning balances are gathered for the accounts hit in the posting_journal
// by querying the period_totals table. If they have never been used, defaults 0. This
// is stored in the variable balance_before.
// 2) The debits and credits of the posting journal are summed for the transactions hit.
// These are grouped by account and joined with the balance_before totals.
// 3) To add clarity, a wrapper SELECT is used to show the balance_before, movements, and then
// balance_final as the sum of all of the above. It also brings in the account_number
const sql = `
SELECT account_id, account.number AS number, account.label AS label,
balance_before, debit_equiv, credit_equiv,
balance_before + debit_equiv - credit_equiv AS balance_final
FROM (
SELECT posting_journal.account_id, totals.balance_before, SUM(debit_equiv) AS debit_equiv,
SUM(credit_equiv) AS credit_equiv
FROM posting_journal JOIN (
SELECT u.account_id, IFNULL(SUM(debit - credit), 0) AS balance_before
FROM (
SELECT DISTINCT account_id FROM posting_journal WHERE posting_journal.trans_id IN (?)
) AS u
LEFT JOIN period_total ON u.account_id = period_total.account_id
GROUP BY u.account_id
) totals ON posting_journal.account_id = totals.account_id
WHERE posting_journal.trans_id IN (?)
GROUP BY posting_journal.account_id
) AS combined
JOIN account ON account.id = combined.account_id
ORDER BY account.number;
`;

// execute the query
db.exec(sql, [transactions, transactions])
.then(data => res.status(200).json(data))
.catch(next);
};
const txn = db.transaction();

/**
* @function checkTransactions
* @descriptions
* fires all check functions and return back an array of promisses containing the errors
* here are the list of checks [type of error]:
*
* 1. A transaction should have at least one line [FATAL]
* 2. A transaction must be balanced [FATAL]
* 3. A transaction must contain unlocked account only [FATAL]
* 4. A transaction must not miss a account [FATAL]
* 5. A transaction must not miss a period or a fiscal year [FATAL]
* 6. A transaction must have every date valid [FATAL]
* 7. A transaction must have a ID for every line [FATAL]
* 8. A transaction must have an ID of an entity for every line which have a no null entity type [FATAL]
* 9. A transaction should have an entity ID if not a warning must be printed, but it is not critical [WARNING]
*
*
* Here is the format of error and warnings :
*
* exceptions : [{
* code : '', // e.g 'MISSING_ACCOUNT'
* fatal : false, // true for error (will block posting) and false for warning (will not block posting)
* transactions : ['HBB1'], // affected transaction ids list
* affectedRows : 12 // number of affectedRows in the transaction
* }]
**/
exports.checkTransactions = function runTrialBalance(req, res, next) {
const transactions = req.body.transactions;
// stage all trial balance transactions
stageTrialBalanceTransactions(txn, transactions);

if (!transactions) {
return next(new BadRequest('The transaction list is null or undefined'));
}
// compute any trial balance errors
txn.addQuery('CALL TrialBalanceErrors()');

if (!Array.isArray(transactions)) {
return next(new BadRequest('The query is bad formatted'));
}
// compute the trial balance summary
txn.addQuery('CALL TrialBalanceSummary()');

const txn = db.transaction();
// compute the aggregate results for the trial balance
// txn.addQuery('CALL TrialBalanceAggregates()');

transactions.forEach((transaction) => {
txn.addQuery('CALL StageTrialBalanceTransaction(?);', transaction);
});
return txn.execute()
.then((results) => {
// because we do not know the number of stageTrialBalance() calls, we must index back by two
// to get to the CALL TrialBalanceErrors() query and one for the Call TrialBalanceSummary()
// query.
const errorsIndex = results.length - 2;
const summaryIndex = results.length - 1;

txn.addQuery('CALL TrialBalance()');
const data = {
errors : results[errorsIndex][0],
summary : results[summaryIndex][0],
};

return txn.execute()
.then((errors) => {
const lastIndex = errors.length - 1;
res.status(201).json(errors[lastIndex][0]);
res.status(201).json(data);
})
.catch(next);
};
Expand All @@ -109,7 +69,7 @@ exports.checkTransactions = function runTrialBalance(req, res, next) {
* @description
* This function can be called only when there is no fatal error
* It posts data to the general ledger.
**/
*/
exports.postToGeneralLedger = function postToGeneralLedger(req, res, next) {
const transactions = req.body.transactions;

Expand Down
85 changes: 50 additions & 35 deletions test/integration/trialBalance.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,80 @@
/* global expect, chai, agent */
/* global expect, agent */

const helpers = require('./helpers');
const uuid = require('node-uuid');

/*
* The /trial_balance API endpoint
*/
describe('(/trial) API endpoint', () => {

const GOOD_TXNS = ['TRANS1', 'TRANS2'];
const UNKNOWN_TXNS = ['TS1', 'TS2'];
describe('(/journal/trialbalance) API endpoint', () => {
const GOOD_TXNS = ['957e4e79-a6bb-4b4d-a8f7-c42152b2c2f6', 'c44619e0-3a88-4754-a750-a414fc9567bf']; // TRANS1, TRANS2
const EMPTY_TXNS = [];
const ERROR_TXNS = ['TRANS5'];
const POSTING_TXNS = ['TRANS1'];

const formatParams = array => ({ transactions: array });
const ERROR_TXNS = ['3688e9ce-85ea-4b5c-9144-688177edcb63']; // TRANS5
const POSTING_TXNS = ['957e4e79-a6bb-4b4d-a8f7-c42152b2c2f6'];

const NUM_ROWS_GOOD_TRANSACTION = 2;
const NUM_ROWS_UNKNOWN_TRANSACTIONS = 0;
const formatParams = transactions => ({ transactions });

it('GET /trial_balance/data_per_account returns data grouped by account ', () => {
return agent.post('/trial_balance/data_per_account')
.send(formatParams(GOOD_TXNS))
it('POST /journal/trialbalance handles empty select with a 400 error', () => {
return agent.post('/journal/trialbalance')
.send(formatParams(EMPTY_TXNS))
.then((res) => {
helpers.api.listed(res, NUM_ROWS_GOOD_TRANSACTION);
helpers.api.errored(res, 400, 'POSTING_JOURNAL.ERRORS.MISSING_TRANSACTIONS');
})
.catch(helpers.handler);
});

it('GET /trial_balance/data_per_account returns an empty array when there is no transaction matching ', () => {
return agent.post('/trial_balance/data_per_account')
.send(formatParams(UNKNOWN_TXNS))
it('POST /journal/trialbalance returns an object with errors and summary information', () => {
return agent.post('/journal/trialbalance')
.send(formatParams(ERROR_TXNS))
.then((res) => {
helpers.api.listed(res, NUM_ROWS_UNKNOWN_TRANSACTIONS);
})
.catch(helpers.handler);
});
expect(res).to.have.status(201);
expect(res).to.be.json;

it('GET /trial_balance/data_per_account returns an error message and 400 code if the request parameter is null or undefined ', () => {
return agent.post('/trial_balance/data_per_account')
.send(formatParams(EMPTY_TXNS))
.then((res) => {
helpers.api.errored(res, 400);
// assert that the returned object has errors and summary properties
expect(res.body).to.have.keys(['errors', 'summary']);

// make sure that TRANS5 sends back an incorrect date error
const err = res.body.errors[0];
expect(err.code).to.equal('POSTING_JOURNAL.ERRORS.DATE_IN_WRONG_PERIOD');
})
.catch(helpers.handler);
});

it('POST /trial_balance/checks returns an array of object containing one or more error object', () => {
return agent.post('/trial_balance/checks')
.send(formatParams(ERROR_TXNS))

it('POST /journal/trialbalance returns summary information grouped by account', () => {
return agent.post('/journal/trialbalance')
.send(formatParams(GOOD_TXNS))
.then((res) => {
expect(res).to.have.status(201);
expect(res).to.be.json;
expect(res.body).to.not.be.empty;
expect(res.body).to.have.length.at.least(1);

// assert that the returned object has errors and summary properties
expect(res.body).to.have.keys(['errors', 'summary']);

// the errors property should be empty
expect(res.body.errors).to.have.length(0);

// The transactions TRANS1, TRANS2 hit 2 accounts and should have the following profiles
const summary = res.body.summary;
expect(summary).to.have.length(2);

// all accounts have 0 balance before
expect(summary[0].balance_before).to.equal(0);
expect(summary[1].balance_before).to.equal(0);

expect(summary[0].debit_equiv).to.equal(100);
expect(summary[1].debit_equiv).to.equal(0);

expect(summary[0].credit_equiv).to.equal(0);
expect(summary[1].credit_equiv).to.equal(100);

expect(summary[0].balance_final).to.equal(100);
expect(summary[1].balance_final).to.equal(-100);
})
.catch(helpers.handler);
});

it('POST /trial_balance/post_transactions posts the a transaction to general_ledger and remove it form the posting_general', () => {
return agent.post('/trial_balance/post_transactions')
it.skip('POST /journal/transactions posts the a transaction to general_ledger and remove it form the posting_general', () => {
return agent.post('/journal/transactions')
.send(formatParams(POSTING_TXNS))
.then((res) => {
expect(res).to.have.status(201);
Expand Down

0 comments on commit b6931a9

Please sign in to comment.