diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..4638d8c --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,17 @@ +env: + es6: true + node: true +extends: 'eslint:recommended' +rules: + indent: + - error + - 2 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always diff --git a/.openshift/gitignore b/.openshift/gitignore deleted file mode 100644 index 9dade01..0000000 --- a/.openshift/gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# will install with 'npm install' -node_modules/ - -# will install after 'npm install' -web/vendor/ diff --git a/.travis.yml b/.travis.yml index e8c4d1f..2cc2e17 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,13 @@ language: node_js node_js: - '6' -- '4' -before_deploy: -- mv .openshift/gitignore .gitignore deploy: provider: openshift user: mugo@forfuture.co.ke password: - secure: UeYKxuX479enwcn1rziErDtnHjR5RFu0a4zK83qZKPhgsXbk+NUXw20fMGtRQvfK/YKm5RW3hqQup6d8dinUJeTqANI4xeunhwV331nWUyzzhRZJUXPb0IiD3idJDpiDDH05leQzSp/8uGt1wb9fYPe1OoN3ylq46RT5kW0tQ/CqkjIOkTPgrX0jY1moANse4RfvGD/6hmgL8nrOM6mqNG2n8fpnoRdznAkZkQlV/vjLEn45SpQEWGmCwn4uFFouXaV3WQMgcuuNvKWQBRS9ZY0VwSNdZoTXvFqaHTxHaPADoMtxszHY18q9XiRpr2TBoBjEXQYtFWBTVN7n3kkQXLHzoMJk/LfsWW3z8lUHVU7Q7MJyuFLj4qRP85neLKQ38CahUCCxbr5IaxvS9/PMysYJi34LYR43SiK2WBf6gdH6SB/GHEJA2IU8TjNmfA0LVbhS/zaDCLD+NRxXghhsaIXAb5JIYfYiOKDwA5WVcIk/Tfz5CpM6M8pYbiP3PYn2bYFr7OY9HL78z0+7NqU4YYiIlgfPf0TKz/FDmtgMJErFNbZbu72uPBryUotjC7zTkSQwtRmyFXGmtc/OpLEnTr3jSK1Sx7BbS1IsravMycNQtzygAykT7SotyhinjE+fXY0fXP59UmQvV/SMjbc6j2GSWsyU4JBF2BV7TpHZMAM= + secure: eVuRZ/p3JerZqPIJxjfITMj4s/mBBfFmyTJNWWotjExdxEPEqxeAJOA5lI6n9d8i6iHUENkpQHJeVzeyMYwd7ove6QkWC1VAK0DHr2siJhrueQIfeX/zNh+aaqxmqYRo+JhneVCEoq/Rr8/NBHTCpg51D1o3WbnCazYzlnnWIAu5rN5LEhLOx3N2EgezkdTbVh82H398P2tyhGHz4qWkzNItVCfRSGsVczSYq3g4qZAQX+pMzG8FWhXZBFiH1SLRAq6Yby2FDkqrsQFJseN/sQx8YSUYscDq+NFjHisSlsvODv2auc8mS40Q1/gvVCK0bmdDBbhpmOBcxUM1xbsyTfYFipE6Ehfw6T+QBjMl1uGW8/s8MvbbcEUe4SDfEugVgIDc8+2zwm2dhWX7kQa2yRwmDzYpWZcVeWphQ95kYkKMiXtjedZWngDSHFjR1OacWtricazXnOmPWQwzXXUQL5Isa+n0KLb8BQDsHHaYSy94cqtGFjSDrCs3X/r6T/jtWtzKul7xth5EMcHn/9DyvCO3F4hAZWb6hwTaQ2ea0J1DM8Cu4J48+Sg2MpgF8Al9HfrcHDKsRicMLuqmxaPkrB2L6qLGc/h6RoLNCIGDl6mn3Af/anQluh6GXnCQum24qk3dmcoJ5Y+NxhikckR2NdNxK5LXnkN3ogpbvwdHZEI= app: mmtcke domain: forfutureco on: repo: forfuturellc/mmtc-ke branch: master - skip_cleanup: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a162a0..5df7de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ This project adheres to [Semantic Versioning](http://semver.org/). +## [0.6.0][0.6.0] - 2016-11-14 + +Added: + +* Add support for **fractional parts** in the `amount`. + +Changed: + +* Drop official support for Node v4.x series + + ## [0.5.0][0.5.0] - 2016-11-11 Added: @@ -74,10 +85,11 @@ Added: This is the very first version. -[Unreleased]: https://github.com/forfuturellc/mmtc-ke/compare/v0.5.0...HEAD [0.0.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.0.0 [0.1.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.1.0 [0.2.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.2.0 [0.3.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.3.0 [0.4.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.4.0 [0.5.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.5.0 +[0.6.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.6.0 +[Unreleased]: https://github.com/forfuturellc/mmtc-ke/compare/v0.6.0...HEAD diff --git a/Gruntfile.js b/Gruntfile.js index 8032a56..4192264 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -5,6 +5,16 @@ exports = module.exports = (grunt) => { loadGruntTasks(grunt); grunt.initConfig({ + eslint: { + src: [ + 'app.js', + 'config/**/*.js', + 'engine/**/*.js', + 'Gruntfile.js', + 'routes/**/*.js', + 'web/js/*.js', + ], + }, sass: { dist: { files: [{ @@ -18,5 +28,7 @@ exports = module.exports = (grunt) => { }, }); - grunt.registerTask("build", ["sass"]); + grunt.registerTask('build', ['sass']); + grunt.registerTask('lint', ['eslint']); + grunt.registerTask('test', ['lint']); }; diff --git a/README.md b/README.md index 947f467..4e4f20a 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ > > Running at **http://mmtc.forfuture.co.ke** +[![Supported Node.js Versions](https://img.shields.io/badge/node->=6-green.svg)](https://github.com/forfuturellc/mmtc-ke) + [![Build Status](https://travis-ci.org/forfuturellc/mmtc-ke.svg?branch=master)](https://travis-ci.org/forfuturellc/mmtc-ke) + + [![Dependency Status](https://gemnasium.com/forfuturellc/mmtc-ke.svg)](https://gemnasium.com/forfuturellc/mmtc-ke) + ## hacking: diff --git a/app.js b/app.js index 3c05d66..6addb5d 100644 --- a/app.js +++ b/app.js @@ -31,7 +31,7 @@ const app = express(); const debug = Debug('mmtc-ke:app'); const logger = engine.clients.getLogger(); let nunjucksEnv; -const devmode = app.get("env") === "development"; +const devmode = app.get('env') === 'development'; debug('initializing engine'); @@ -40,16 +40,16 @@ engine.init(); debug('configuring nunjucks'); nunjucksEnv = nunjucks.configure('web', { - autoescape: true, - express: app, - noCache: devmode ? true : false, + autoescape: true, + express: app, + noCache: devmode ? true : false, }); -debug("adding global variables for nunjucks templates"); -nunjucksEnv.addGlobal("pkg", pkg); -nunjucksEnv.addGlobal("site", config.get("site")); -nunjucksEnv.addGlobal("env", process.env); +debug('adding global variables for nunjucks templates'); +nunjucksEnv.addGlobal('pkg', pkg); +nunjucksEnv.addGlobal('site', config.get('site')); +nunjucksEnv.addGlobal('env', process.env); debug('setting up views'); @@ -61,12 +61,12 @@ app.set('view engine', 'html'); debug('disabling Express view cache'); -app.set("view cache", false); +app.set('view cache', false); debug('mounting middleware for parsing request body'); app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: false })); +app.use(bodyParser.urlencoded({ extended: false })); debug('mounting middleware for serving static files'); @@ -78,24 +78,24 @@ app.use(routes); debug('mounting catch-all handler'); -app.use(function(req, res, next) { - return routes.utils.renderPage(req, res, 'error', { - error: new engine.errors.PageNotFoundError(`page '${req.path}' not found`), - }); +app.use(function(req, res) { + return routes.utils.renderPage(req, res, 'error', { + error: new engine.errors.PageNotFoundError(`page '${req.path}' not found`), + }); }); debug('mounting middleware for error handling'); -app.use(function(err, req, res, next) { - logger.error(err); - return routes.utils.renderPage(req, res, 'error', { - error: err, - }); +app.use(function(err, req, res, next) { // eslint-disable-line no-unused-vars + logger.error(err); + return routes.utils.renderPage(req, res, 'error', { + error: err, + }); }); debug('starting server'); app.listen(config.get('server.port'), config.get('server.ip'), function() { - logger.info('server listening'); - debug('server started at http://%s:%s', config.get('server.ip'), config.get('server.port')); + logger.info('server listening'); + debug('server started at http://%s:%s', config.get('server.ip'), config.get('server.port')); }); diff --git a/config/default.js b/config/default.js index be8eae3..4dc4c48 100644 --- a/config/default.js +++ b/config/default.js @@ -19,12 +19,12 @@ config.server.ip = process.env.OPENSHIFT_NODEJS_IP || '127.0.0.1'; // site configuration config.site = {}; -config.site.title = `Mobile Money Transaction Cost in Kenya`; -config.site.title_short = `mmtc | ke`; -config.site.email = `we@forfuture.co.ke`; -config.site.description = `An easy way to calculate cost of mobile money transcations in Kenya` -config.site.url = `mmtc.forfuture.co.ke`; -config.site.baseurl = ``; +config.site.title = 'Mobile Money Transaction Cost in Kenya'; +config.site.title_short = 'mmtc | ke'; +config.site.email = 'we@forfuture.co.ke'; +config.site.description = 'An easy way to calculate cost of mobile money transcations in Kenya'; +config.site.url = 'mmtc.forfuture.co.ke'; +config.site.baseurl = ''; config.site.author = {}; config.site.author.name = 'Forfuture LLC'; config.site.author.url = 'http://forfuture.co.ke'; diff --git a/data/SPEC.md b/data/SPEC.md index df2a9d4..3ec579a 100644 --- a/data/SPEC.md +++ b/data/SPEC.md @@ -2,7 +2,7 @@ |Aspect|Detail| |------|------| -|Version|0.4| +|Version|0.5| |Written by|GochoMugo | The data used in the application in its computations is fed through data files @@ -81,7 +81,6 @@ additions: * `-2`: raises `AmountNotFoundError`, inferring that the amount for this transaction can **not** be determined using our data (depends on external factors, e.g. merchant reputation) -* **no** fractional part Therefore, the cost is accurate to **1 KES**. diff --git a/data/airtel-money.json b/data/airtel-money.json index 06a58bc..e50d7a0 100644 --- a/data/airtel-money.json +++ b/data/airtel-money.json @@ -1,5 +1,5 @@ { - "name": "airtel money", + "name": "airtel-money", "meta": { "spec": "0.2", "date_updated": "2016-06-16", diff --git a/data/equitel.json b/data/equitel.json new file mode 100644 index 0000000..19d2743 --- /dev/null +++ b/data/equitel.json @@ -0,0 +1,67 @@ +{ + "name": "equitel", + "meta": { + "spec": "0.5", + "date_updated": "2016-11-11", + "url": "http://www.equitel.com/my-money/rates" + }, + "transactions": [ + { + "name": "transfer", + "classes": [ + { + "name": "equitel or orange money", + "ranges": [ + { "low": "-Infinity", "high": 49, "amount": -1 }, + { "low": 50, "high": 35000, "amount": 0 }, + { "low": 35001, "high": "+Infinity", "amount": -1 } + ] + }, + { + "name": "mpesa or airtel money", + "ranges": [ + { "low": "-Infinity", "high": 49, "amount": -1 }, + { "low": 50, "high": 100, "amount": 34.1 }, + { "low": 101, "high": 500, "amount": 38.5 }, + { "low": 501, "high": 1000, "amount": 44 }, + { "low": 1001, "high": 1500, "amount": 49.5 }, + { "low": 1501, "high": 35000, "amount": 60.5 }, + { "low": 35001, "high": "+Infinity", "amount": -1 } + ] + } + ] + }, + { + "name": "others", + "amount_input": false, + "classes": [ + { + "name": "deposits", + "amount": 0 + }, + { + "name": "transaction reports", + "amount": 0 + }, + { + "name": "airtime", + "amount": 0 + }, + { + "name": "balance enquiry", + "amount": 0 + }, + { + "name": "pay bills", + "amount": 0 + }, + { + "name": "atm withdrawal", + "amount": 33 + } + ] + } + ], + "ussd_codes": [ + ] +} diff --git a/data/mpesa.json b/data/mpesa.json index eb3c325..60e5b1e 100644 --- a/data/mpesa.json +++ b/data/mpesa.json @@ -1,7 +1,7 @@ { "name": "mpesa", "meta": { - "spec": "0.2", + "spec": "0.4", "date_updated": "2016-11-11", "url": "http://www.safaricom.co.ke/personal/m-pesa" }, diff --git a/engine/math.js b/engine/math.js index 436b39e..83dabdf 100644 --- a/engine/math.js +++ b/engine/math.js @@ -23,6 +23,10 @@ exports = module.exports = { * transaction was not found * @throws RangeNotFoundError if the amount was not found in any range * @throws InvalidAmountError if the amount entered was invalid + * @throws AmountNotAllowedError if the amount is not allowed for the + * transaction + * @throws AmountNotFoundError if the amount can not be determined using + * the data available to the engine */ calculate: calculate, /** @@ -75,7 +79,7 @@ function calculate(name, params) { let amount, range; - amount = parseInt(params.amount, 10); + amount = Number(params.amount); if (amount < 0) { throw new errors.InvalidAmountError(`amount '${params.amount}' is not valid`); } @@ -90,9 +94,9 @@ function calculate(name, params) { switch (range.amount) { case -1: - throw new errors.AmountNotAllowedError(`amount is not allowed`); + throw new errors.AmountNotAllowedError('amount is not allowed'); case -2: - throw new errors.AmountNotFoundError(range.message || `amount not found`); + throw new errors.AmountNotFoundError(range.message || 'amount not found'); } return range.amount; @@ -118,6 +122,6 @@ function parseRange(range) { } else if (n === '+Infinity') { return +Infinity; } - return parseInt(n, 10); + return Number(n); } } diff --git a/package.json b/package.json index e7779d3..c76c5c6 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,26 @@ { "name": "mmtc-ke", - "version": "0.5.0", + "version": "0.6.0", "private": true, "scripts": { "build": "grunt build", "postinstall": "HOME=${BOWER_HOME:-${HOME}} bower install", "start": "forever app.js", "start-dev": "DEBUG=mmtc-ke:* nodemon app.js", - "test": "echo no tests!!!" + "test": "grunt test" }, "dependencies": { "body-parser": "^1.15.2", - "bower": "^1.7.7", + "bower": "^1.8.0", "common-errors": "^1.0.0", - "config": "^1.19.0", - "debug": "^2.2.0", + "config": "^1.24.0", + "debug": "^2.3.2", "express": "^4.14.0", - "forever": "^0.15.1", - "lodash": "^4.15.0", - "nunjucks": "^2.3.0", - "showdown": "^1.4.3", - "winston": "^2.2.0" + "forever": "^0.15.3", + "lodash": "^4.17.0", + "nunjucks": "^3.0.0", + "showdown": "^1.5.0", + "winston": "^2.3.0" }, "repository": { "type": "git", @@ -32,13 +32,14 @@ "url": "http://www.gmugo.in" }, "engines": { - "node": "4.2.x" + "node": ">=6" }, "devDependencies": { "grunt": "^1.0.1", "grunt-cli": "^1.2.0", + "grunt-eslint": "^19.0.0", "grunt-sass": "^1.2.1", "load-grunt-tasks": "^3.5.2", - "nodemon": "^1.10.2" + "nodemon": "^1.11.0" } } diff --git a/routes/api.js b/routes/api.js index 56ea12d..a0f8513 100644 --- a/routes/api.js +++ b/routes/api.js @@ -13,7 +13,6 @@ const path = require('path'); // npm-installed modules const _ = require('lodash'); -const Debug = require('debug'); const express = require('express'); @@ -23,7 +22,6 @@ const utils = require('./utils'); // module variables -const debug = Debug('mmtc-ke:routes:api'); const router = express.Router(); const logger = engine.clients.getLogger(); @@ -41,7 +39,7 @@ router.get('/', function(req, res, next) { // serving data for all networks -router.get('/networks', function(req, res, next) { +router.get('/networks', function(req, res) { return res.json({ networks: engine.networks.getNetworks(), }); @@ -93,7 +91,7 @@ router.use(function(req, res, next) { // API Error handler -router.use(function(error, req, res, next) { +router.use(function(error, req, res, next) { // eslint-disable-line no-unused-vars error.statusCode = error.statusCode || 500; if (error.statusCode >= 500) { logger.error(error); diff --git a/routes/public.js b/routes/public.js index 7f46f47..7e330cf 100644 --- a/routes/public.js +++ b/routes/public.js @@ -13,7 +13,6 @@ const path = require('path'); // npm-installed modules const _ = require('lodash'); -const Debug = require('debug'); const express = require('express'); @@ -23,7 +22,6 @@ const utils = require('./utils'); // module variables -const debug = Debug("mmtc-ke:routes:public"); const router = express.Router(); @@ -33,84 +31,84 @@ exports.router = router; // home page -router.get("/", function(req, res) { - return utils.renderPage(req, res, 'index', { - networks: engine.networks.getNetworks(), - }); +router.get('/', function(req, res) { + return utils.renderPage(req, res, 'index', { + networks: engine.networks.getNetworks(), + }); }); // network page router - .route('/n/:name') - .get(function(req, res) { - return renderNetworkPage(req, res); - }) - .post(function(req, res) { - if (!_.isString(req.body.amount)) { - return renderNetworkPage(req, res, { - result: { - error: true, - message: 'invalid amount', - }, - }); - } - if (!_.isString(req.body.transactionType)) { - return renderNetworkPage(req, res, { - result: { - error: true, - message: 'invalid transaction type', - }, - }); - } - if (!_.isString(req.body.transactor)) { - return renderNetworkPage(req, res, { - result: { - error: true, - message: 'invalid transactor', - }, - }); - } - - let cost; - - try { - cost = engine.math.calculate(req.params.name, req.body); - } catch(err) { - return renderNetworkPage(req, res, { - result: { - error: true, - message: err.message, - }, - }); - } - - return renderNetworkPage(req, res, { - defaults: { - amount: req.body.amount, - }, - result: { - success: true, - cost, - }, - }); - }); - function renderNetworkPage(req, res, ctx) { - const network = engine.networks.getNetwork(req.params.name); - - if (!network) { - return utils.renderPage(req, res, 'error', { - error: new engine.errors.NetworkNotFoundError(`network '${req.params.name}' not found`), - }); - } - - return utils.renderPage(req, res, 'networks/index', _.assign(ctx || {}, { - networks: engine.networks.getNetworks(), - network, - body: _.isEmpty(req.body) ? null : req.body, - })); + .route('/n/:name') + .get(function(req, res) { + return renderNetworkPage(req, res); + }) + .post(function(req, res) { + if (!_.isString(req.body.amount)) { + return renderNetworkPage(req, res, { + result: { + error: true, + message: 'invalid amount', + }, + }); + } + if (!_.isString(req.body.transactionType)) { + return renderNetworkPage(req, res, { + result: { + error: true, + message: 'invalid transaction type', + }, + }); + } + if (!_.isString(req.body.transactor)) { + return renderNetworkPage(req, res, { + result: { + error: true, + message: 'invalid transactor', + }, + }); + } + + let cost; + + try { + cost = engine.math.calculate(req.params.name, req.body); + } catch(err) { + return renderNetworkPage(req, res, { + result: { + error: true, + message: err.message, + }, + }); } + return renderNetworkPage(req, res, { + defaults: { + amount: req.body.amount, + }, + result: { + success: true, + cost, + }, + }); + }); +function renderNetworkPage(req, res, ctx) { + const network = engine.networks.getNetwork(req.params.name); + + if (!network) { + return utils.renderPage(req, res, 'error', { + error: new engine.errors.NetworkNotFoundError(`network '${req.params.name}' not found`), + }); + } + + return utils.renderPage(req, res, 'networks/index', _.assign(ctx || {}, { + networks: engine.networks.getNetworks(), + network, + body: _.isEmpty(req.body) ? null : req.body, + })); +} + // News page router.get('/news', function(req, res, next) { diff --git a/routes/utils.js b/routes/utils.js index 0516288..d490ad6 100644 --- a/routes/utils.js +++ b/routes/utils.js @@ -42,12 +42,11 @@ const path = require('path'); // npm-installed modules const _ = require('lodash'); -const config = require('config'); const express = require('express'); // own modules -const engine = require("../engine"); +const engine = require('../engine'); function webMiddleware(router, config) { diff --git a/web/.eslintrc.yml b/web/.eslintrc.yml new file mode 100644 index 0000000..f0f5725 --- /dev/null +++ b/web/.eslintrc.yml @@ -0,0 +1,17 @@ +env: + browser: true + jquery: true +extends: 'eslint:recommended' +rules: + indent: + - error + - 2 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always diff --git a/web/js/network.js b/web/js/network.js index ced4227..5b57ee9 100644 --- a/web/js/network.js +++ b/web/js/network.js @@ -8,7 +8,7 @@ $(document).ready(function() { - "use strict"; + 'use strict'; var $select = $('.select-class'); @@ -16,15 +16,15 @@ $(document).ready(function() { return tableActivate(this); }); - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + $('a[data-toggle="tab"]').on('shown.bs.tab', function() { var selects = $(this).find('.select-class'); if (selects.length === 0) return; return tableActivate(selects[0]); }); function tableActivate(select) { - var $option = $($(select).find('option:selected')[0]); - $('.tab-pane.active .table-ranges.active').removeClass('active'); - $($option.data('table')).addClass('active'); + var $option = $($(select).find('option:selected')[0]); + $('.tab-pane.active .table-ranges.active').removeClass('active'); + $($option.data('table')).addClass('active'); } });