diff --git a/README.md b/README.md index 80cf40f5..ddc0eb57 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,11 @@ The following endpoints are supported: * Journals * Organisations * Payments +* TaxRates * TrackingCategories (and TrackingOptions) * Users -The following endpoints are included but are not currently tested or supported for use: +The following endpoints are included but are not currently tested nor are they supported for use: * Attachments * Timesheets (Payroll API) * Employees (Payroll API) @@ -102,28 +103,43 @@ Runscope is not endorsed by or affiliated with Xero. This tool was used by the S ## Private App Usage ```javascript -var PrivateApplication = require('xero-node').PrivateApplication; -var privateApp = new PrivateApplication(); +var xero = require('xero-node'); +var fs = require('fs'); +var config = require('/some/path/to/config.js'); -// This checks the ~/.xero/config.json directory by default looking for a config file. -// Alternatively a path to a JSON file can be provided as a parameter: +//Private key can either be a path or a String so check both variables and make sure the path has been parsed. +if (config.privateKeyPath && !config.privateKey) + config.privateKey = fs.readFileSync(config.privateKeyPath); -var myConfigFile = "/tmp/config.json"; -var xeroClient = new PrivateApplication(myConfigFile); +var xeroClient = new xero.PrivateApplication(config); ``` ## Pubic Usage ```javascript -var PublicApplication = require('xero-node').PublicApplication; -var xeroClient = new PublicApplication(myConfigFile); +var xero = require('xero-node'); +var fs = require('fs'); +var config = require('/some/path/to/config.js'); + +//Private key can either be a path or a String so check both variables and make sure the path has been parsed. +if (config.privateKeyPath && !config.privateKey) + config.privateKey = fs.readFileSync(config.privateKeyPath); + +var xeroClient = new xero.PublicApplication(myConfigFile); ``` ## Partner Usage ```javascript -var ParnerApplication = require('xero-node').PartnerApplication; -var xeroClient = new PartnerApplication(myConfigFile); +var xero = require('xero-node'); +var fs = require('fs'); +var config = require('/some/path/to/config.js'); + +//Private key can either be a path or a String so check both variables and make sure the path has been parsed. +if (config.privateKeyPath && !config.privateKey) + config.privateKey = fs.readFileSync(config.privateKeyPath); + +var xeroClient = new xero.PartnerApplication(myConfigFile); ``` Examples @@ -136,7 +152,7 @@ xeroClient.core.invoices.getInvoices() .then(function(invoices) { console.log("Invoices: " + invoices.length); }).catch(function(err) { - console.error(err); + console.log(err); }); ``` @@ -191,15 +207,119 @@ xeroClient.core.contacts.getContacts({ npm test -## Release History +## Release Change Log * 1.0.0 - - Merged master branch from guillegette - - Merged master branch from elliots - - Externalised configs for private apps (keys should not live in the code) - - Fixed the private app 'consumerKey' issue - - Fixed the logger so it correctly supports different log levels - - Added support for runscope urls in the signature generation + - [view](http://github.com/jordanwalsh23/xero-node/commit/0e9444051537806b5567c08080cd95b93449cbdf) Externalised the config file for private apps, fixed the log level settings, updated the tests to use 'should' library, added support for runscope urls within the signature generation + - [view](http://github.com/jordanwalsh23/xero-node/commit/88d22ffee782288bf375462396490dfb21e7fd15) updated readme + - [view](http://github.com/jordanwalsh23/xero-node/commit/fa4ed825995e8fdbfb2e257a72f34696ce5d91fe) updated tests to check each field of an account + - [view](http://github.com/jordanwalsh23/xero-node/commit/0497a8ae7d68549a8e8358151cf5c340af74bf6a) added account tests. Currently there is an issue creating new accounts: BankAccountNumber is not being sent through. Needs investigation + - [view](http://github.com/jordanwalsh23/xero-node/commit/17ed6605f6fd84bfc875e87c81c00b8fab830ca5) added more tests for accounts. Fixed bug around passing BankAccountNumbers. TODO: Delete method hasn't been implemented globally. + - [view](http://github.com/jordanwalsh23/xero-node/commit/12a60708fd8448fad90444b842613e34d642bfe5) added support for Payments, however this is still in progress. + - [view](http://github.com/jordanwalsh23/xero-node/commit/72546b964ff12a664573a77e41ab5ad6e9c1c1c6) fixed bug with payments, these are working as expected + - [view](http://github.com/jordanwalsh23/xero-node/commit/aff762aedf4583f4ff637662ddd52dcb466d4bbe) updated banktransactions to copy contact objects from XML correctly + - [view](http://github.com/jordanwalsh23/xero-node/commit/cb8a1a06e5aefc60fbd669a343fb3efeb7dae2a2) removed guid from test + - [view](http://github.com/jordanwalsh23/xero-node/commit/4464acf9576aff61fa09b2cca9eb2a229c357244) changed tests to all running + - [view](http://github.com/jordanwalsh23/xero-node/commit/e1fce607e9928d60b1f9fd1831ae50208900cf45) added support for invoice rounding + - [view](http://github.com/jordanwalsh23/xero-node/commit/0df589e3d39c5522549d2d5f53f4a9b4ff27ba12) updated DP rounding fix to remove double querystring additions + - [view](http://github.com/jordanwalsh23/xero-node/commit/eefd34a7535236d14edcc8b02b08e3dc64649f77) added support for externalising user-agent header in config.json file + - [view](http://github.com/jordanwalsh23/xero-node/commit/edd51f9a5e512b4e2db787dc27feed63cf6c944b) added zombie support to gain access tokens on public apps + - [view](http://github.com/jordanwalsh23/xero-node/commit/f947eced4aa0e6ac304c9a0e2c2190af37564d41) added support for istanbul test reporting, and completed support for public app auth + - [view](http://github.com/jordanwalsh23/xero-node/commit/2b8b65a27799298e1a6e4e1e262725490d8c9972) fixed issue with banktransfers fromXML function, updated tests to pass above 80% + - [view](http://github.com/jordanwalsh23/xero-node/commit/c4a9b248977466e0d2709e4e3630ef8cff8abd8c) fixed tests for contacts and added more address information to the schema + - [view](http://github.com/jordanwalsh23/xero-node/commit/b5b82aedf87bfbb156f935b9a78b06cc8a82821c) updated items to support retrieving and saving + - [view](http://github.com/jordanwalsh23/xero-node/commit/5a64852bd5056dbe3d1044bb3f96fba06689af1f) updated various elements to have consistent responses on when save() is called. Updated Items tests to have 100% coverage.. woohoo + - [view](http://github.com/jordanwalsh23/xero-node/commit/efed46b222c898a1f63789ca2db1293537de752d) externalised the runscope bucket ID to the config file + - [view](http://github.com/jordanwalsh23/xero-node/commit/251b0839f5ab641f6a5bcf120d964765beaf9fd0) fixed the saveContacts method on the contacts object and did some refactoring. This concept could be applied across all endpoints. Also removed some console.log statments from the code + - [view](http://github.com/jordanwalsh23/xero-node/commit/a2409633a78825880f85c7ac776a90262dd7b6c5) added tests for Journals + - [view](http://github.com/jordanwalsh23/xero-node/commit/882fedf9f995d2eeedfec4da9a92f360ff0e1eca) added payment tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/34ab49da6e823c343b9b6298d40d39d8fcae46ab) added support for tracking categories but tracking options is not currently working + - [view](http://github.com/jordanwalsh23/xero-node/commit/86c57337933acf057776f40aee48fc1c59577497) added support for tracking categories and tracking options + - [view](http://github.com/jordanwalsh23/xero-node/commit/16fff713121faa84dd831318ceb62422ed6bf5b3) Added attachment Tests but these aren't currently working + - [view](http://github.com/jordanwalsh23/xero-node/commit/65d964acb25a95597c96f6363be4ad8a09c363e0) Updated readme + - [view](http://github.com/jordanwalsh23/xero-node/commit/57066c8b89dc4e8a1fe1bd7f26cf0965cea33dcf) updated readme + - [view](http://github.com/jordanwalsh23/xero-node/commit/8a310f27e7b0a5720f84a6b175f019f4338a35b5) added support for Partner application types + - [view](http://github.com/jordanwalsh23/xero-node/commit/ccbb239995e75abbc290031342cbfeca749c1f9e) updated sample app to get this working + - [view](http://github.com/jordanwalsh23/xero-node/commit/5cf2235050be389387d76de8b7b5430c649455c4) Updated tests to override the default callback URL + - [view](http://github.com/jordanwalsh23/xero-node/commit/e2253364db39707e5f240218b74c040573f58301) Updated the sample app to use a bootstrap theme. + - [view](http://github.com/jordanwalsh23/xero-node/commit/d093f8d3bcf8914ae4b15b5ea18f81eb7466406b) Added more support for features in the sample app + - [view](http://github.com/jordanwalsh23/xero-node/commit/d5952b32bc69c58a317ffd9568baf73520badf4a) Added support for the remaining endpoints to sample app + - [view](http://github.com/jordanwalsh23/xero-node/commit/cfc846f639b3f41d59694b125a346a2fae58fdca) Added navigation highlights + - [view](http://github.com/jordanwalsh23/xero-node/commit/7e8a62d64e4d140edf34a7a9ecdb34e847d90186) Updated oauth_test to sample_app + - [view](http://github.com/jordanwalsh23/xero-node/commit/29f80b01ed42fda48df3be4048f6cf7827d961b3) Merge pull request #3 from jordanwalsh23/v1.0.0 + - [view](http://github.com/jordanwalsh23/xero-node/commit/5f719b131ac84cc5206f11e6aaf23bcb415ef4ca) added various tests and updated sample app + - [view](http://github.com/jordanwalsh23/xero-node/commit/be1dac5e9aca3f0700fbef535016bf2340fd85ca) Merge pull request #4 from jordanwalsh23/v1.0.0 + - [view](http://github.com/jordanwalsh23/xero-node/commit/ca30b9bdf02765f30930633cadb2d05889ca3cf6) updated tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/d79da2117cc7e87de1a6a31f66692c53df3de994) adding tax rates + - [view](http://github.com/jordanwalsh23/xero-node/commit/dc6cb40de1dd464b51674f8956356a881ff75d19) Reflect the fact that issues are switched off + - [view](http://github.com/jordanwalsh23/xero-node/commit/642ad9b52b5c9fec1bba75deb5fb4ef95cccd831) Reflect deprecation of entrust certs + - [view](http://github.com/jordanwalsh23/xero-node/commit/c9d1ce3179ad07ccc18929cc1ffff3feed74da7b) Merge pull request #5 from davidbanham/no-issue + - [view](http://github.com/jordanwalsh23/xero-node/commit/6b56e2b5c91b2aaf269a81125690c140c6aae5df) Change user-specific config paths + - [view](http://github.com/jordanwalsh23/xero-node/commit/9246c0b6b5f60e4950998762d8b06e177e0ce186) Move config setup to before hook + - [view](http://github.com/jordanwalsh23/xero-node/commit/01c815f30aed8d597fcae005aab23bd6fc2636ab) Increase timeout for token tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/b972d2b43758e1ec02ff7a7c3c0c09424d4ab794) Drop org selection + - [view](http://github.com/jordanwalsh23/xero-node/commit/8c03f1287157a38e0fdaac67d9fc329e06227cdf) updated gitignore to ignore *config.json files + - [view](http://github.com/jordanwalsh23/xero-node/commit/7956c1fbe2bb73bb0f8fcada1db7b66aebd028f5) Merge branch 'master' of github.com:jordanwalsh23/xero-node + - [view](http://github.com/jordanwalsh23/xero-node/commit/3dbbc3293277ee82854a051bc6ab74b89a3d693a) Merge pull request #6 from davidbanham/deprecate-entrust + - [view](http://github.com/jordanwalsh23/xero-node/commit/5918bb842f2f0f6c7727181d22c3ab87e2ce5b44) Merge branch 'master' of github.com:jordanwalsh23/xero-node + - [view](http://github.com/jordanwalsh23/xero-node/commit/c75e2da0e49afcf0b250aa928a58e96829815795) Explain test failure for tax rates get test + - [view](http://github.com/jordanwalsh23/xero-node/commit/2ced01354d1123026cfdf4cc08758d3476388d17) Fix reference to undefined `obj` + - [view](http://github.com/jordanwalsh23/xero-node/commit/d83c845c9be87b088ce8feeec6afb501879efb85) Throw errors instead of objects. + - [view](http://github.com/jordanwalsh23/xero-node/commit/03d0052e450ce87da0b117dd93355acf2f79780a) fixing taxrates + - [view](http://github.com/jordanwalsh23/xero-node/commit/e2c8efcd5846531573ef2149a8471521aabd1196) Merge pull request #7 from davidbanham/fix-errors + - [view](http://github.com/jordanwalsh23/xero-node/commit/8e76123b19cb786cc50b14fa4172d03e7e0d386d) Merge branch 'master' of github.com:jordanwalsh23/xero-node + - [view](http://github.com/jordanwalsh23/xero-node/commit/23949da7dda02196e2ca9544cc347e844e38bc84) fixed taxrates test + - [view](http://github.com/jordanwalsh23/xero-node/commit/2a4dc93ab95d0d64c98c7e35544a0364afb36616) Add missing space to error message + - [view](http://github.com/jordanwalsh23/xero-node/commit/6b96d4260ba4f23163e7d8745a8b696ec0232708) Add account creation hooks to payment tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/f0ab37b8a94192d6c46f4280d267b118f938a2b2) Set timeout globally + - [view](http://github.com/jordanwalsh23/xero-node/commit/5771b5c3a3a200c1940c30438ee47c4318ec7f6d) Add account creation hooks to bank transaction testing + - [view](http://github.com/jordanwalsh23/xero-node/commit/1828d87af188e2fa6cb8f72bbc32d5ee90f633fc) Use created bank accounts for bank transfers tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/5f2f480cca18ca483860a8c1b2b1d0e81ef175e0) Add setup and teardown of tracking categories for region test + - [view](http://github.com/jordanwalsh23/xero-node/commit/4adccb897a3318d3951851cfea1b51e1b5effc92) Make accounts test repeatable + - [view](http://github.com/jordanwalsh23/xero-node/commit/28d361cd732be9f7d1b5377d933d786e3fb7656f) Unskip working tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/f96b5a330a8f4411575273a6099ddf63dc2b5166) Unskip tax rate test after rebase + - [view](http://github.com/jordanwalsh23/xero-node/commit/5d31493d777e40b46d185f38108bc57cbb97fece) Merge branch 'davidbanham-refactor-tests' + - [view](http://github.com/jordanwalsh23/xero-node/commit/308b85dcb9c1611db1ed293f77bf7e7054aa5abc) s/fail/catch/g + - [view](http://github.com/jordanwalsh23/xero-node/commit/36484a5e5ab12d01d47f2bae466856fd968b6ea3) Add entropy to updated name + - [view](http://github.com/jordanwalsh23/xero-node/commit/761b2c6e42e300b70ad2a43a682333adf9c37a22) Switch application.js to native Promises + - [view](http://github.com/jordanwalsh23/xero-node/commit/7bbd1bf04b9eda400154af74889a7848037e2bce) Complete switch to native promises + - [view](http://github.com/jordanwalsh23/xero-node/commit/0d92db48ded79bb999fb3ec22e6087ab759f3d54) Add editorconfig for 4 space tabs + - [view](http://github.com/jordanwalsh23/xero-node/commit/2e38ead301e2b1638295fe0a978843a344c58e18) Add yarn lockfile + - [view](http://github.com/jordanwalsh23/xero-node/commit/3d6b613eccb05cf533043b76fc2ce8f9a1759507) Merge branch 'davidbanham-promises' + - [view](http://github.com/jordanwalsh23/xero-node/commit/8c55b7249afc7a3916f3396113c4735366e759bd) Merge branch 'meta' of git://github.com/davidbanham/xero-node into davidbanham-meta + - [view](http://github.com/jordanwalsh23/xero-node/commit/0f44ca7eeba70156cf0d657d58e7887c918dec35) Merge branch 'davidbanham-meta' + - [view](http://github.com/jordanwalsh23/xero-node/commit/01fa26d7494b43327bb08efb370149829f447fd9) Pass config variables rather than reading file from disc. + - [view](http://github.com/jordanwalsh23/xero-node/commit/35554a0201a0408d12a8178d7a547a91c287c9ff) Just use camelCase rather than PascalCase in passed config + - [view](http://github.com/jordanwalsh23/xero-node/commit/aadad1e7d5b98ff05cea072156261e1b931e8f9d) s/_.extend/Object.assign/g + - [view](http://github.com/jordanwalsh23/xero-node/commit/a439c1407b123e85891fda828054487f2aedf753) updated sample app to include taxrates and users + - [view](http://github.com/jordanwalsh23/xero-node/commit/535e15866215ebb261c2fabd6a060624757ae53a) Merge branch 'config' of git://github.com/davidbanham/xero-node into davidbanham-config + - [view](http://github.com/jordanwalsh23/xero-node/commit/8a51f1f7bcddde0fcefd92878cf74a260a84c13e) updated sample app and removed console.log from taxrate + - [view](http://github.com/jordanwalsh23/xero-node/commit/f732c5ea6dab546ea889d5210c8d57bc2d33df8d) Merge branch 'davidbanham-config' + - [view](http://github.com/jordanwalsh23/xero-node/commit/1b0a79dad13ab97fc22476308770fb944b559657) Merge branch 'less-lodash' of git://github.com/davidbanham/xero-node into davidbanham-less-lodash + - [view](http://github.com/jordanwalsh23/xero-node/commit/560950b4b8bfd8d4301cb5607e592269e8f380a9) migrated config files to their own directories + - [view](http://github.com/jordanwalsh23/xero-node/commit/619146e95126b17132b14b124f2536efe82e63da) Merged dabvidbanhan-less-lodash + - [view](http://github.com/jordanwalsh23/xero-node/commit/72937fe13d490037d91dbbb597e70f01f63930b3) updated sample_app.js to index.js + - [view](http://github.com/jordanwalsh23/xero-node/commit/7325004388adf28a185a09b3222dd43bdf8f3832) removed comments from json file + - [view](http://github.com/jordanwalsh23/xero-node/commit/762770eaab898ca33c518a92619562cbf2828e69) working on refresh functions + - [view](http://github.com/jordanwalsh23/xero-node/commit/9bcd056a85bf6a6d4e772208168b7292e5571fb1) implemented refresh function. Need to detect unauthorized API call and automatically refresh the tokens + - [view](http://github.com/jordanwalsh23/xero-node/commit/7dd4b15f59dd08aa57119ecb35a0a68f0aa7eeb0) added automatic refresh on the GET function, haven't tested it properly yet + - [view](http://github.com/jordanwalsh23/xero-node/commit/804e36ebaf1ad927580af1804b05ad516f7d909a) added refresh and check expiry functions. Also updated the sample app + - [view](http://github.com/jordanwalsh23/xero-node/commit/52ff3909e1ea29847f3b3e251de51ffff7b9034e) Minor cleanup of promise/callback confusion in tests + - [view](http://github.com/jordanwalsh23/xero-node/commit/71c2f6ecf70ef0b3a2d66bc51a2e94d34464b64f) Merge branch 'davidbanham-cleaner-tests' + - [view](http://github.com/jordanwalsh23/xero-node/commit/a73f23b4dbadd275af13f98669ae2aca66cac448) Merge branch 'master' into add_refresh_token_functionality + - [view](http://github.com/jordanwalsh23/xero-node/commit/c17877bd291c08a54b3f0a180611e94ac48f6e23) Added event emitter to send updated tokens. + - [view](http://github.com/jordanwalsh23/xero-node/commit/752f05a88415da37416712a96d1e35affee854b3) merged promise/callback fix + - [view](http://github.com/jordanwalsh23/xero-node/commit/bf5236023353a14b6aa94100e931dbd5c68bcf83) fixed test file merge issue + - [view](http://github.com/jordanwalsh23/xero-node/commit/acb9aeb6abb639830c2c2f479ba79eb848309ca2) added tax rates save and delete functions + - [view](http://github.com/jordanwalsh23/xero-node/commit/53a47c94b1b9a1f28a8e2b39e65e730ef06f1599) updated yarn.lockfile + - [view](http://github.com/jordanwalsh23/xero-node/commit/96d890aee3c5229bf564a7c88118b6626def182c) added the 'after' function to cleanup the test accounts + - [view](http://github.com/jordanwalsh23/xero-node/commit/95bd79c3819e1f3ab0cc8f2059d76e1d5117ec7f) Merge pull request #18 from jordanwalsh23/accounts_setup_teardown_cleanup + - [view](http://github.com/jordanwalsh23/xero-node/commit/5e2805d3fbb6f6094897e41428a7f4b57fb3e43f) re-added the tests after commenting them out + - [view](http://github.com/jordanwalsh23/xero-node/commit/003da15fe1b02a12051f48772082ce3f41355a61) Merge pull request #17 from jordanwalsh23/add_taxrate_support + - [view](http://github.com/jordanwalsh23/xero-node/commit/04099590532e6558103f9789e5a24883effd0666) Merge branch 'master' into add_refresh_token_functionality + - [view](http://github.com/jordanwalsh23/xero-node/commit/ae43d41b82a4f7af4db20262b25f470afe1d0e1f) reverted forced timeout of the token + - [view](http://github.com/jordanwalsh23/xero-node/commit/ed4d797ffec2bd27308b104d6c198166d5910b1b) Merge branch 'add_refresh_token_functionality' of github.com:jordanwalsh23/xero-node into add_refresh_token_functionality + - [view](http://github.com/jordanwalsh23/xero-node/commit/d2bff73444196a6441121d0fff3d85e529705a0e) Merge pull request #14 from jordanwalsh23/add_refresh_token_functionality * 0.0.2 - Added journals - modifiedAfter support diff --git a/lib/application.js b/lib/application.js index 0c89e9a6..8da8d59d 100644 --- a/lib/application.js +++ b/lib/application.js @@ -8,7 +8,8 @@ var _ = require('lodash'), Core = require('./core'), qs = require('querystring'), Payroll = require('./payroll'), - xml2js = require('xml2js') + xml2js = require('xml2js'), + events = new require('events'); function Batch(application) { logger.debug('Batch::constructor'); @@ -45,6 +46,7 @@ function Application(options) { var core = new Core(this); var payroll = new Payroll(this); + Object.defineProperties(this, { core: { get: function() { @@ -81,6 +83,8 @@ Object.assign(Application.prototype, { if (this.options["runscopeBucketId"] && this.options["runscopeBucketId"] !== "") { this.options.baseUrl = "https://api-xero-com-" + this.options["runscopeBucketId"] + ".runscope.net"; } + + this.eventEmitter = new events.EventEmitter(); }, post: function(path, body, options, callback) { return this.putOrPost('post', path, body, options, callback); @@ -98,55 +102,73 @@ Object.assign(Application.prototype, { options = options || {}; return new Promise(function(resolve, reject) { var params = {}; - if (options.summarizeErrors === false) - params.summarizeErrors = false; - //Added to support more than 2dp being added. - if (options.unitdp) - params.unitdp = options.unitdp; + self.checkExpiry() + .then(function() { + if (options.summarizeErrors === false) + params.summarizeErrors = false; - var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; - var url = self.options.baseUrl + endPointUrl + path; - if (!_.isEmpty(params)) - url += '?' + querystring.stringify(params); + //Added to support more than 2dp being added. + if (options.unitdp) + params.unitdp = options.unitdp; - self.oa[method](url, self.options.accessToken, self.options.accessSecret, { xml: body }, function(err, data, res) { + var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; + var url = self.options.baseUrl + endPointUrl + path; + if (!_.isEmpty(params)) + url += '?' + querystring.stringify(params); - if (err && data && data.indexOf('oauth_problem') >= 0) { - var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); - errObj.data = qs.parse(data); - reject(errObj); - callback && callback(errObj); - return; - } + self.oa[method](url, self.options.accessToken, self.options.accessSecret, { xml: body }, function(err, data, res) { - self.xml2js(data) - .then(function(obj) { - if (err) { - var exception = ""; - if (obj.ApiException) - exception = obj.ApiException; - else if (obj.Response.ErrorNumber) - exception = obj.Response; - var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode + ' and exception: ' + JSON.stringify(exception, null, 2)); + if (err && data && data.indexOf('oauth_problem') >= 0) { + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); + errObj.data = qs.parse(data); reject(errObj); callback && callback(errObj); - } else { - var ret = { response: obj.Response, res: res }; - if (options.entityConstructor) { - ret.entities = self.convertEntities(obj.Response, options); - } - resolve(ret); - callback && callback(null, obj, res, ret.entities); + return; } - }) - .catch(function(err) { - logger.error(err); - throw err; - }) + self.xml2js(data) + .then(function(obj) { + if (err) { + var exception = ""; + if (obj.ApiException) + exception = obj.ApiException; + else if (obj.Response.ErrorNumber) + exception = obj.Response; + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode + ' and exception: ' + JSON.stringify(exception, null, 2)); + reject(errObj); + callback && callback(errObj); + } else { + var ret = { response: obj.Response, res: res }; + if (options.entityConstructor) { + ret.entities = self.convertEntities(obj.Response, options); + } + resolve(ret); + callback && callback(null, obj, res, ret.entities); + } + + }) + .catch(function(err) { + logger.error(err); + throw err; + }) + + }); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error(method.toUpperCase() + ' call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } + }); + - }); }); }, delete: function(path, options, callback) { @@ -157,42 +179,57 @@ Object.assign(Application.prototype, { var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; var url = self.options.baseUrl + endPointUrl + path; - self.oa.delete(url, self.options.accessToken, self.options.accessSecret, function(err, data, res) { - if (options.stream && !err) { - // Already done - return resolve(); - } - if (err && data && data.indexOf('oauth_problem') >= 0) { - var errObj = new Error('DELETE call failed with: ' + err.statusCode); - errObj.data = qs.parse(data); - reject(errObj); - callback && callback(errObj); - return; - } + self.checkExpiry() + .then(function() { + self.oa.delete(url, self.options.accessToken, self.options.accessSecret, function(err, data, res) { + if (options.stream && !err) { + // Already done + return resolve(); + } + if (err && data && data.indexOf('oauth_problem') >= 0) { + var errObj = new Error('DELETE call failed with: ' + err.statusCode); + errObj.data = qs.parse(data); + reject(errObj); + callback && callback(errObj); + return; + } - if (err) { - var errObj = new Error('DELETE call failed with: ' + err.statusCode + ' and message: ' + err.data); - reject(errObj); - callback && callback(errObj); - return; - } + if (err) { + var errObj = new Error('DELETE call failed with: ' + err.statusCode + ' and message: ' + err.data); + reject(errObj); + callback && callback(errObj); + return; + } - //Some delete operations don't return any content (e.g. HTTP204) so simply resolve the promise - if (!data || data === "") { - return resolve(); - } + //Some delete operations don't return any content (e.g. HTTP204) so simply resolve the promise + if (!data || data === "") { + return resolve(); + } - self.xml2js(data) - .then(function(obj) { - var ret = { response: obj.Response, res: res }; - resolve(ret); - callback && callback(null, obj, res); - }) - .catch(function(err) { - logger.error(err); - throw err; - }) - }, { stream: options.stream }); + self.xml2js(data) + .then(function(obj) { + var ret = { response: obj.Response, res: res }; + resolve(ret); + callback && callback(null, obj, res); + }) + .catch(function(err) { + logger.error(err); + throw err; + }) + }, { stream: options.stream }); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error('DELETE call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } + }); }); }, get: function(path, options, callback) { @@ -207,10 +244,25 @@ Object.assign(Application.prototype, { if (options.format) self.oa._headers['Accept'] = 'application/' + options.format; - if (options.pager) - getResource(options.pager.start || 1) - else - getResource(); + self.checkExpiry() + .then(function() { + if (options.pager) + getResource(options.pager.start || 1) + else + getResource(); + }) + .catch(function(err) { + logger.debug(err); + if (err && err.data) { + var dataParts = qs.parse(err.data); + + var errObj = new Error('GET call failed with: ' + err.statusCode); + errObj.data = dataParts; + reject(errObj); + callback && callback(errObj); + return; + } + }); function getResource(offset) { var endPointUrl = options.api === 'payroll' ? self.options.payrollAPIEndPointUrl : self.options.coreAPIEndPointUrl; @@ -243,11 +295,7 @@ Object.assign(Application.prototype, { return resolve(); } if (err && data) { - var dataParts; - if (_.isObject(data)) - dataParts = qs.parse(data); - else - dataParts = data; + var dataParts = qs.parse(data); var errObj = new Error('GET call failed with: ' + err.statusCode); errObj.data = dataParts; @@ -417,6 +465,31 @@ Object.assign(Application.prototype, { var builder = new xml2js.Builder({ rootName: rootName, headless: true }); var obj = builder.buildObject(obj); return obj; + }, + checkExpiry: function() { + + /** + * CheckExpiry is a helper function that will compare the current token expiry to the current time. + * + * As there is potential for a time difference, instead of waiting all the way until the current time + * has passed the expiry time, we instead add 3 minutes to the current time, and use that as a comparison. + * + * This ensures that if the token is 'nearing' the expiry, it'll attempt to be refreshed. + */ + + var expiry = new Date(this.options.tokenExpiry), + checkTime = addMinutes(new Date(), 3); + + if (checkTime >= expiry) { + logger.debug("Refreshing Access Token"); + return this.refreshAccessToken(); + } else { + return Promise.resolve(); + } + + function addMinutes(date, minutes) { + return new Date(date.getTime() + minutes * 60000); + } } }) @@ -471,15 +544,47 @@ var RequireAuthorizationApplication = Application.extend({ }); }); }, - getAccessToken: function(token, secret, verifier, callback, options) { + setAccessToken: function(token, secret, verifier, callback, options) { var self = this; return new Promise(function(resolve, reject) { - self.oa.getOAuthAccessToken(token, secret, verifier, function(err, token, secret, results) { + self.oa.getOAuthAccessToken(token, secret, verifier, function(err, results) { if (err) reject(err); - else - resolve({ token: token, secret: secret, results: results }); + else { + var exp = new Date(); + exp.setTime(exp.getTime() + (results.oauth_expires_in * 1000)); + self.setOptions({ + accessToken: results.oauth_token, + accessSecret: results.oauth_token_secret, + sessionHandle: results.oauth_session_handle, + tokenExpiry: exp.toString() + }, false); + resolve({ results: results }); + } + callback && callback.apply(callback, arguments); + }) + }); + }, + refreshAccessToken: function(callback, options) { + var self = this; + + return new Promise(function(resolve, reject) { + self.oa.getOAuthAccessToken(self.options.accessToken, self.options.accessSecret, { oauth_session_handle: self.options.sessionHandle }, function(err, results) { + if (err) + reject(err); + else { + var exp = new Date(); + exp.setTime(exp.getTime() + (results.oauth_expires_in * 1000)); + self.setOptions({ + accessToken: results.oauth_token, + accessSecret: results.oauth_token_secret, + sessionHandle: results.oauth_session_handle, + tokenExpiry: exp.toString() + }, true); + resolve({ results: results }); + } + callback && callback.apply(callback, arguments); }) }); @@ -488,9 +593,21 @@ var RequireAuthorizationApplication = Application.extend({ var q = Object.assign({}, { oauth_token: requestToken }, other); return this.options.baseUrl + this.options.authorizeUrl + '?' + querystring.stringify(q); }, - setOptions: function(options) { - this.options.accessToken = options.accessToken; - this.options.accessSecret = options.accessSecret; + setOptions: function(options, emitThisEvent) { + logger.debug("Setting options"); + options.accessToken ? this.options.accessToken = options.accessToken : false; + options.accessSecret ? this.options.accessSecret = options.accessSecret : false; + options.sessionHandle ? this.options.sessionHandle = options.sessionHandle : false; + options.tokenExpiry ? this.options.tokenExpiry = options.tokenExpiry : false; + + if (emitThisEvent && this.eventEmitter) { + logger.debug("Emitting event"); + try { + this.eventEmitter.emit('xeroTokenUpdate', options); + } catch (e) { + logger.error(e); + } + } } }); diff --git a/lib/entities/accounting/taxrate.js b/lib/entities/accounting/taxrate.js index 7cab9dfb..6ce37522 100644 --- a/lib/entities/accounting/taxrate.js +++ b/lib/entities/accounting/taxrate.js @@ -13,7 +13,8 @@ var TaxRateSchema = new Entity.SchemaObject({ DisplayTaxRate: { type: Number, toObject: 'always' }, EffectiveRate: { type: Number, toObject: 'always' }, Status: { type: String, toObject: 'always' }, - TaxComponents: { type: Array, arrayType: TaxComponentSchema, toObject: 'always' } + TaxComponents: { type: Array, arrayType: TaxComponentSchema, toObject: 'always' }, + ReportTaxType: { type: String, toObject: 'hasValue' } }); var TaxComponentSchema = new Entity.SchemaObject({ @@ -38,8 +39,28 @@ var TaxRate = Entity.extend(TaxRateSchema, { return this; }, toXml: function() { - var taxrate = _.omit(this.toObject()); - return this.application.js2xml(taxrate, 'TaxRate'); + var taxRate = _.omit(this.toObject(), 'TaxComponents'); + Object.assign(taxRate, { TaxComponents: [] }); + _.forEach(this.TaxComponents, function(taxComponent) { + taxRate.TaxComponents.push({ TaxComponent: taxComponent }) + }) + return this.application.js2xml(taxRate, 'TaxRate'); + }, + save: function(options) { + var self = this; + var xml = '' + this.toXml() + ''; + var path = 'TaxRates', + method; + if (this.Status) { + method = 'post' + } else { + method = 'put' + } + return this.application.putOrPostEntity(method, path, xml, { entityPath: 'TaxRates.TaxRate', entityConstructor: function(data) { return self.application.core.taxrates.newTaxRate(data) } }); + }, + delete: function(options) { + this.Status = "DELETED"; + return this.save(); } }); diff --git a/lib/oauth/oauth.js b/lib/oauth/oauth.js index 7d7417d8..0753a064 100644 --- a/lib/oauth/oauth.js +++ b/lib/oauth/oauth.js @@ -461,6 +461,8 @@ exports.OAuth.prototype.getOAuthAccessToken = function(oauth_token, oauth_token_ var extraParams = {}; if (typeof oauth_verifier == "function") { callback = oauth_verifier; + } else if (typeof oauth_verifier == "object") { + extraParams.oauth_session_handle = oauth_verifier.oauth_session_handle; } else { extraParams.oauth_verifier = oauth_verifier; } @@ -469,11 +471,7 @@ exports.OAuth.prototype.getOAuthAccessToken = function(oauth_token, oauth_token_ if (error) callback(error); else { var results = querystring.parse(data); - var oauth_access_token = results["oauth_token"]; - delete results["oauth_token"]; - var oauth_access_token_secret = results["oauth_token_secret"]; - delete results["oauth_token_secret"]; - callback(null, oauth_access_token, oauth_access_token_secret, results); + callback(null, results); } }) } @@ -584,4 +582,4 @@ exports.OAuth.prototype.authHeader = function(url, oauth_token, oauth_token_secr var orderedParameters = this._prepareParameters(oauth_token, oauth_token_secret, method, url, {}); return this._buildAuthorizationHeaders(orderedParameters); -}; +}; \ No newline at end of file diff --git a/package.json b/package.json index e3856ea6..2a44be9b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "license": "MIT", "dependencies": { "dateformat": "~1.0.7-1.2.3", + "events": "^1.1.1", "lodash": "~2.4.1", "log4js": "~0.6.9", "lru-cache": "^4.0.2", @@ -39,6 +40,7 @@ "mustache": "^2.3.0", "nodemailer": "", "nyc": "^10.1.2", + "sinon": "^1.17.7", "zombie": "^5.0.5", "zombie-phantom": "0.0.6" } diff --git a/sample_app/index.js b/sample_app/index.js index be1ed3b0..65f9e448 100644 --- a/sample_app/index.js +++ b/sample_app/index.js @@ -6,18 +6,42 @@ var express = require('express'), nodemailer = require('nodemailer'), metaConfig = require('./config/config.json'); -function getXeroApp(session) { - var APPTYPE = metaConfig.APPTYPE; - var config = metaConfig[APPTYPE.toLowerCase()]; - - if (session) { - if (session.oauthAccessToken && session.oauthAccessSecret) { - config.accessToken = session.oauthAccessToken; - config.accessSecret = session.oauthAccessSecret; +var xeroClient; +var eventReceiver; + +function getXeroClient(session) { + + if (!xeroClient) { + var APPTYPE = metaConfig.APPTYPE; + var config = metaConfig[APPTYPE.toLowerCase()]; + + if (session) { + if (session.oauthAccessToken && session.oauthAccessSecret) { + config.accessToken = session.oauthAccessToken; + config.accessSecret = session.oauthAccessSecret; + } + } + + if (config.privateKeyPath && !config.privateKey) config.privateKey = fs.readFileSync(config.privateKeyPath); + + switch (APPTYPE) { + case "PUBLIC": + xeroClient = new xero.PublicApplication(config); + break; + case "PARTNER": + xeroClient = new xero.PartnerApplication(config); + eventReceiver = xeroClient.eventEmitter; + eventReceiver.on('xeroTokenUpdate', function(data) { + //Store the data that was received from the xeroTokenRefresh event + console.log("Received xero token refresh: ", data); + }); + break; + default: + throw "No App Type Set!!" } } - return new xero.PublicApplication(config); + return xeroClient; } var app = express(); @@ -72,8 +96,8 @@ app.use(express.static(__dirname + '/assets')); // app.use(express.cookieSession({ secret: 'sfsdfsdfsdfsdf234234234fd', cookie: { maxAge: 123467654456 } })); function authorizeRedirect(req, res, returnTo) { - var xeroApp = getXeroApp(null, returnTo); - xeroApp.getRequestToken(function(err, token, secret) { + var xeroClient = getXeroClient(null, returnTo); + xeroClient.getRequestToken(function(err, token, secret) { if (!err) { req.session.oauthRequestToken = token; req.session.oauthRequestSecret = secret; @@ -83,7 +107,7 @@ function authorizeRedirect(req, res, returnTo) { var PayrollScope = 'payroll.employees,payroll.payitems,payroll.timesheets'; var AccountingScope = ''; - var authoriseUrl = xeroApp.buildAuthorizeUrl(token, { + var authoriseUrl = xeroClient.buildAuthorizeUrl(token, { scope: PayrollScope }); res.redirect(authoriseUrl); @@ -97,13 +121,27 @@ function authorizeRedirect(req, res, returnTo) { var cache = LRU(); function authorizedOperation(req, res, returnTo, callback) { - if (req.session.oauthAccessToken) { - var xeroApp = getXeroApp(req.session); - callback(xeroApp); - } else + if (xeroClient) { + callback(xeroClient); + } else { authorizeRedirect(req, res, returnTo); + } +} + +function handleErr(err, req, res, returnTo) { + console.log(err); + if (err.data && err.data.oauth_problem && err.data.oauth_problem == "token_rejected") { + authorizeRedirect(req, res, returnTo); + } else { + res.redirect('error'); + } } +app.get('/error', function(req, res) { + console.log(req.query.error); + res.render('index', { error: req.query.error }); +}) + // Home Page app.get('/', function(req, res) { res.render('index', { @@ -115,23 +153,23 @@ app.get('/', function(req, res) { // Redirected from xero with oauth results app.get('/access', function(req, res) { - var xeroApp = getXeroApp(); + var xeroClient = getXeroClient(); if (req.query.oauth_verifier && req.query.oauth_token == req.session.oauthRequestToken) { - xeroApp.getAccessToken(req.session.oauthRequestToken, req.session.oauthRequestSecret, req.query.oauth_verifier, - function(err, accessToken, accessSecret, results) { - req.session.oauthAccessToken = accessToken; - req.session.oauthAccessSecret = accessSecret; + xeroClient.setAccessToken(req.session.oauthRequestToken, req.session.oauthRequestSecret, req.query.oauth_verifier) + .then(function() { var returnTo = req.session.returnto; res.redirect(returnTo || '/'); - } - ) + }) + .catch(function(err) { + handleErr(err, req, res, 'error'); + }) } }); app.get('/organisations', function(req, res) { - authorizedOperation(req, res, '/organisations', function(xeroApp) { - xeroApp.core.organisations.getOrganisations() + authorizedOperation(req, res, '/organisations', function(xeroClient) { + xeroClient.core.organisations.getOrganisations() .then(function(organisations) { res.render('organisations', { organisations: organisations, @@ -141,14 +179,14 @@ app.get('/organisations', function(req, res) { }); }) .catch(function(err) { - console.log(err); + handleErr(err, req, res, 'organisations'); }) }) }); app.get('/taxrates', function(req, res) { - authorizedOperation(req, res, '/taxrates', function(xeroApp) { - xeroApp.core.taxrates.getTaxRates() + authorizedOperation(req, res, '/taxrates', function(xeroClient) { + xeroClient.core.taxrates.getTaxRates() .then(function(taxrates) { res.render('taxrates', { taxrates: taxrates, @@ -157,12 +195,15 @@ app.get('/taxrates', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'taxrates'); + }) }) }); app.get('/users', function(req, res) { - authorizedOperation(req, res, '/users', function(xeroApp) { - xeroApp.core.users.getUsers() + authorizedOperation(req, res, '/users', function(xeroClient) { + xeroClient.core.users.getUsers() .then(function(users) { res.render('users', { users: users, @@ -171,12 +212,15 @@ app.get('/users', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'users'); + }) }) }); app.get('/employees', function(req, res) { - authorizedOperation(req, res, '/employees', function(xeroApp) { - xeroApp.payroll.employees.getEmployees() + authorizedOperation(req, res, '/employees', function(xeroClient) { + xeroClient.payroll.employees.getEmployees() .then(function(employees) { res.render('employees', { employees: employees, @@ -186,26 +230,15 @@ app.get('/employees', function(req, res) { }); }) .catch(function(err) { - console.log(err) - res.render('employees', { - error: err, - active: { - employees: true - } - }) + handleErr(err, req, res, 'employees'); }) }) }); -app.get('/error', function(req, res) { - console.log(req.query.error); - res.render('index', { error: req.query.error }); -}) - app.get('/contacts', function(req, res) { - authorizedOperation(req, res, '/contacts', function(xeroApp) { + authorizedOperation(req, res, '/contacts', function(xeroClient) { var contacts = []; - xeroApp.core.contacts.getContacts({ pager: { callback: pagerCallback } }) + xeroClient.core.contacts.getContacts({ pager: { callback: pagerCallback } }) .then(function() { res.render('contacts', { contacts: contacts, @@ -214,6 +247,9 @@ app.get('/contacts', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'contacts'); + }) function pagerCallback(err, response, cb) { contacts.push.apply(contacts, response.data); @@ -223,9 +259,9 @@ app.get('/contacts', function(req, res) { }); app.get('/banktransactions', function(req, res) { - authorizedOperation(req, res, '/banktransactions', function(xeroApp) { + authorizedOperation(req, res, '/banktransactions', function(xeroClient) { var bankTransactions = []; - xeroApp.core.bankTransactions.getBankTransactions({ pager: { callback: pagerCallback } }) + xeroClient.core.bankTransactions.getBankTransactions({ pager: { callback: pagerCallback } }) .then(function() { res.render('banktransactions', { bankTransactions: bankTransactions, @@ -234,6 +270,9 @@ app.get('/banktransactions', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'banktransactions'); + }) function pagerCallback(err, response, cb) { bankTransactions.push.apply(bankTransactions, response.data); @@ -243,9 +282,9 @@ app.get('/banktransactions', function(req, res) { }); app.get('/journals', function(req, res) { - authorizedOperation(req, res, '/journals', function(xeroApp) { + authorizedOperation(req, res, '/journals', function(xeroClient) { var journals = []; - xeroApp.core.journals.getJournals({ pager: { callback: pagerCallback } }) + xeroClient.core.journals.getJournals({ pager: { callback: pagerCallback } }) .then(function() { res.render('journals', { journals: journals, @@ -254,6 +293,9 @@ app.get('/journals', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'journals'); + }) function pagerCallback(err, response, cb) { journals.push.apply(journals, response.data); @@ -263,9 +305,9 @@ app.get('/journals', function(req, res) { }); app.get('/banktransfers', function(req, res) { - authorizedOperation(req, res, '/banktransfers', function(xeroApp) { + authorizedOperation(req, res, '/banktransfers', function(xeroClient) { var bankTransfers = []; - xeroApp.core.bankTransfers.getBankTransfers({ pager: { callback: pagerCallback } }) + xeroClient.core.bankTransfers.getBankTransfers({ pager: { callback: pagerCallback } }) .then(function() { res.render('banktransfers', { bankTransfers: bankTransfers, @@ -274,6 +316,9 @@ app.get('/banktransfers', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'banktransfers'); + }) function pagerCallback(err, response, cb) { bankTransfers.push.apply(bankTransfers, response.data); @@ -283,8 +328,8 @@ app.get('/banktransfers', function(req, res) { }); app.get('/payments', function(req, res) { - authorizedOperation(req, res, '/payments', function(xeroApp) { - xeroApp.core.payments.getPayments() + authorizedOperation(req, res, '/payments', function(xeroClient) { + xeroClient.core.payments.getPayments() .then(function(payments) { res.render('payments', { payments: payments, @@ -293,12 +338,15 @@ app.get('/payments', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'payments'); + }) }) }); app.get('/trackingcategories', function(req, res) { - authorizedOperation(req, res, '/trackingcategories', function(xeroApp) { - xeroApp.core.trackingCategories.getTrackingCategories() + authorizedOperation(req, res, '/trackingcategories', function(xeroClient) { + xeroClient.core.trackingCategories.getTrackingCategories() .then(function(trackingcategories) { res.render('trackingcategories', { trackingcategories: trackingcategories, @@ -307,12 +355,15 @@ app.get('/trackingcategories', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'trackingcategories'); + }) }) }); app.get('/accounts', function(req, res) { - authorizedOperation(req, res, '/accounts', function(xeroApp) { - xeroApp.core.accounts.getAccounts() + authorizedOperation(req, res, '/accounts', function(xeroClient) { + xeroClient.core.accounts.getAccounts() .then(function(accounts) { res.render('accounts', { accounts: accounts, @@ -321,13 +372,16 @@ app.get('/accounts', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'accounts'); + }) }) }); app.get('/timesheets', function(req, res) { - authorizedOperation(req, res, '/timesheets', function(xeroApp) { - xeroApp.payroll.timesheets.getTimesheets() + authorizedOperation(req, res, '/timesheets', function(xeroClient) { + xeroClient.payroll.timesheets.getTimesheets() .then(function(timesheets) { res.render('timesheets', { timesheets: timesheets, @@ -337,48 +391,17 @@ app.get('/timesheets', function(req, res) { }); }) .catch(function(err) { - console.log(err) - res.render('timesheets', { - error: err, - active: { - timesheets: true - } - }) + handleErr(err, req, res, 'timesheets'); }) }) }); -app.use('/createtimesheet', function(req, res) { - if (req.method == 'GET') { - return res.render('createtimesheet'); - } else if (req.method == 'POST') { - authorizedOperation(req, res, '/createtimesheet', function(xeroApp) { - var timesheet = xeroApp.payroll.timesheets.newTimesheet({ - EmployeeID: '065a115c-ba9c-4c03-b8e3-44c551ed8f21', - StartDate: new Date(2014, 8, 23), - EndDate: new Date(2014, 8, 29), - Status: 'Draft', - TimesheetLines: [{ - EarningsTypeID: 'a9ab82bf-c421-4840-b245-1df307c2127a', - NumberOfUnits: [5, 0, 0, 0, 0, 0, 0] - }] - }); - timesheet.save() - .then(function(ret) { - res.render('createtimesheet', { timesheets: ret.entities }) - }) - .catch(function(err) { - res.render('createtimesheet', { err: err }) - }) - }) - } -}); app.get('/invoices', function(req, res) { - authorizedOperation(req, res, '/invoices', function(xeroApp) { - xeroApp.core.invoices.getInvoices() + authorizedOperation(req, res, '/invoices', function(xeroClient) { + xeroClient.core.invoices.getInvoices() .then(function(invoices) { console.log(invoices[0].Payments[0]); res.render('invoices', { @@ -388,13 +411,16 @@ app.get('/invoices', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'invoices'); + }) }) }); app.get('/items', function(req, res) { - authorizedOperation(req, res, '/items', function(xeroApp) { - xeroApp.core.items.getItems() + authorizedOperation(req, res, '/items', function(xeroClient) { + xeroClient.core.items.getItems() .then(function(items) { res.render('items', { items: items, @@ -403,6 +429,9 @@ app.get('/items', function(req, res) { } }); }) + .catch(function(err) { + handleErr(err, req, res, 'items'); + }) }) }); @@ -411,8 +440,8 @@ app.use('/createinvoice', function(req, res) { if (req.method == 'GET') { return res.render('createinvoice'); } else if (req.method == 'POST') { - authorizedOperation(req, res, '/createinvoice', function(xeroApp) { - var invoice = xeroApp.core.invoices.newInvoice({ + authorizedOperation(req, res, '/createinvoice', function(xeroClient) { + var invoice = xeroClient.core.invoices.newInvoice({ Type: req.body.Type, Contact: { Name: req.body.Contact @@ -439,13 +468,40 @@ app.use('/createinvoice', function(req, res) { } }); +app.use('/createtimesheet', function(req, res) { + if (req.method == 'GET') { + return res.render('createtimesheet'); + } else if (req.method == 'POST') { + authorizedOperation(req, res, '/createtimesheet', function(xeroClient) { + var timesheet = xeroClient.payroll.timesheets.newTimesheet({ + EmployeeID: '065a115c-ba9c-4c03-b8e3-44c551ed8f21', + StartDate: new Date(2014, 8, 23), + EndDate: new Date(2014, 8, 29), + Status: 'Draft', + TimesheetLines: [{ + EarningsTypeID: 'a9ab82bf-c421-4840-b245-1df307c2127a', + NumberOfUnits: [5, 0, 0, 0, 0, 0, 0] + }] + }); + timesheet.save() + .then(function(ret) { + res.render('createtimesheet', { timesheets: ret.entities }) + }) + .catch(function(err) { + res.render('createtimesheet', { err: err }) + }) + + }) + } +}); + app.use('/emailinvoice', function(req, res) { if (req.method == 'GET' && !req.query.a) { res.render('emailinvoice', { id: req.query.id }); } else { - authorizedOperation(req, res, '/emailinvoice?id=' + req.query.id + '&a=1&email=' + encodeURIComponent(req.body.Email), function(xeroApp) { + authorizedOperation(req, res, '/emailinvoice?id=' + req.query.id + '&a=1&email=' + encodeURIComponent(req.body.Email), function(xeroClient) { var file = fs.createWriteStream(__dirname + '/invoice.pdf', { encoding: 'binary' }); - xeroApp.core.invoices.streamInvoice(req.query.id, 'pdf', file); + xeroClient.core.invoices.streamInvoice(req.query.id, 'pdf', file); file.on('finish', function() { var transporter = nodemailer.createTransport(); // Direct var mailOptions = { diff --git a/test/accountingtests.js b/test/accountingtests.js index 13c340ef..76a2e4a5 100644 --- a/test/accountingtests.js +++ b/test/accountingtests.js @@ -1,6 +1,7 @@ var chai = require('chai'), should = chai.should(), expect = chai.expect, + sinon = require('sinon'), _ = require('lodash'), xero = require('..'), util = require('util'), @@ -14,6 +15,7 @@ process.on('uncaughtException', function(err) { }) var currentApp; +var eventReceiver; var organisationCountry = ''; var APPTYPE = metaConfig.APPTYPE; @@ -36,6 +38,8 @@ before('init instance and set options', function(done) { throw "No App Type Set!!" } + eventReceiver = currentApp.eventEmitter; + done(); }) @@ -47,7 +51,6 @@ describe('get access for public or partner application', function() { }); describe('Get tokens', function() { - var authoriseUrl = ""; var requestToken = ""; var requestSecret = ""; @@ -56,6 +59,26 @@ describe('get access for public or partner application', function() { var accessToken = ""; var accessSecret = ""; + //This function is used by the event emitter to receive the event when the token + //is automatically refreshed. We use the 'spy' function so that we can include + //some checks within the tests. + var spy = sinon.spy(function() { + console.log("Event Received. Creating new Partner App"); + + //Create a new application object when we receive new tokens + currentApp = new xero.PartnerApplication(config); + currentApp.setOptions(arguments[0]); + //Reset the event receiver so the listener stack is shared correctly. + eventReceiver = currentApp.eventEmitter; + eventReceiver.on('xeroTokenUpdate', function(data) { console.log("Event Received: ", data); }); + + console.log("Partner app recreated"); + }); + + it('adds the event listener', function(done) { + eventReceiver.on('xeroTokenUpdate', spy); + done(); + }); it('user gets a token and builds the url', function() { return currentApp.getRequestToken() @@ -77,7 +100,7 @@ describe('get access for public or partner application', function() { runScripts: false }); - browser.debug(); + //browser.debug(); before(function(done) { if (APPTYPE === "PRIVATE") { @@ -147,16 +170,51 @@ describe('get access for public or partner application', function() { }); describe('swaps the request token for an access token', function() { - it('calls the access token method and sets the options', function() { - return currentApp.getAccessToken(requestToken, requestSecret, verifier) - .then(function({ token, secret }) { - currentApp.setOptions({ accessToken: token, accessSecret: secret }); + it('calls the access token method and sets the options', function(done) { + currentApp.setAccessToken(requestToken, requestSecret, verifier) + .then(function() { + expect(currentApp.options.accessToken).to.not.equal(undefined); + expect(currentApp.options.accessToken).to.not.equal(""); + expect(currentApp.options.accessSecret).to.not.equal(undefined); + expect(currentApp.options.accessSecret).to.not.equal(""); + + if (APPTYPE === "PARTNER") { + expect(currentApp.options.sessionHandle).to.not.equal(undefined); + expect(currentApp.options.sessionHandle).to.not.equal(""); + } + + done(); + }).catch(function(err) { + done(wrapError(err)); + }); + }); + + it('refreshes the token', function(done) { + if (APPTYPE !== "PARTNER") { + this.skip(); + } + + //Only supported for Partner integrations + currentApp.refreshAccessToken() + .then(function() { + expect(currentApp.options.accessToken).to.not.equal(undefined); + expect(currentApp.options.accessToken).to.not.equal(""); + expect(currentApp.options.accessSecret).to.not.equal(undefined); + expect(currentApp.options.accessSecret).to.not.equal(""); + expect(currentApp.options.sessionHandle).to.not.equal(undefined); + expect(currentApp.options.sessionHandle).to.not.equal(""); + + expect(spy.called).to.equal(true); + done(); + }).catch(function(err) { + done(wrapError(err)); }); }); }); }); }); + describe('reporting tests', function() { this.timeout(10000); it('generates a Balance Sheet', function(done) { @@ -181,7 +239,6 @@ describe('reporting tests', function() { }) describe.skip('regression tests', function() { - var InvoiceID = ""; var PaymentID = ""; @@ -231,12 +288,23 @@ describe.skip('regression tests', function() { }); }); - // There appears to be no way to archive a bank account via the API - // after('archive the test account', function() { - // testAccount.Status = 'ARCHIVED'; - // return testAccount.save(); - // }); + // There appears to be no way to archive a bank account via the API so deleting instead + after('delete the test accounts', function() { + bankAccounts.forEach(function(account) { + + currentApp.core.accounts.deleteAccount(account.id) + .then(function(response) { + expect(response.Status).to.equal("OK"); + done(); + }) + .catch(function(err) { + console.log(util.inspect(err, null, null)); + done(wrapError(err)); + }); + }); + + }); describe('organisations', function() { it('get', function(done) { @@ -260,6 +328,9 @@ describe.skip('regression tests', function() { }) describe('taxrates', function() { + + var createdTaxRate; + it('gets tax rates', function(done) { currentApp.core.taxrates.getTaxRates() .then(function(taxRates) { @@ -293,7 +364,68 @@ describe.skip('regression tests', function() { console.log(err); done(wrapError(err)); }) - }) + }); + + it('creates a new tax rate', function(done) { + var taxrate = { + Name: '20% GST on Expenses', + TaxComponents: [{ + Name: 'GST', + Rate: 20.1234, + IsCompound: false + }], + ReportTaxType: 'INPUT' + } + + var taxRate = currentApp.core.taxrates.newTaxRate(taxrate); + + taxRate.save() + .then(function(response) { + expect(response.entities).to.have.length.greaterThan(0); + createdTaxRate = response.entities[0]; + + expect(createdTaxRate.Name).to.equal(taxrate.Name); + expect(createdTaxRate.TaxType).to.match(/TAX[0-9]{3}/); + expect(createdTaxRate.CanApplyToAssets).to.be.oneOf([true, false]); + expect(createdTaxRate.CanApplyToEquity).to.be.oneOf([true, false]); + expect(createdTaxRate.CanApplyToExpenses).to.be.oneOf([true, false]); + expect(createdTaxRate.CanApplyToLiabilities).to.be.oneOf([true, false]); + expect(createdTaxRate.CanApplyToRevenue).to.be.oneOf([true, false]); + expect(createdTaxRate.DisplayTaxRate).to.equal(taxrate.TaxComponents[0].Rate); + expect(createdTaxRate.EffectiveRate).to.equal(taxrate.TaxComponents[0].Rate); + expect(createdTaxRate.Status).to.equal('ACTIVE'); + expect(createdTaxRate.ReportTaxType).to.equal(taxrate.ReportTaxType); + + createdTaxRate.TaxComponents.forEach(function(taxComponent) { + expect(taxComponent.Name).to.equal(taxrate.TaxComponents[0].Name); + + //This is hacked toString() because of: https://github.com/jordanwalsh23/xero-node/issues/13 + expect(taxComponent.Rate).to.equal(taxrate.TaxComponents[0].Rate.toString()); + expect(taxComponent.IsCompound).to.equal(taxrate.TaxComponents[0].IsCompound.toString()); + }); + done(); + }) + .catch(function(err) { + console.log(err); + done(wrapError(err)); + }) + }); + + it('updates the taxrate to DELETED', function(done) { + + createdTaxRate.delete() + .then(function(response) { + expect(response.entities).to.have.lengthOf(1); + expect(response.entities[0].Status).to.equal("DELETED"); + done(); + }) + .catch(function(err) { + console.log(err); + done(wrapError(err)); + }) + + }); + }); describe('accounts', function() { @@ -582,7 +714,37 @@ describe.skip('regression tests', function() { .catch(function(err) { done(wrapError(err)); }) - }) + }); + + it('saves multiple invoices', function(done) { + var invoices = []; + + for (var i = 0; i < 10; i++) { + invoices.push(currentApp.core.invoices.newInvoice({ + Type: 'ACCREC', + Contact: { + Name: 'Department of Testing' + }, + DueDate: new Date().toISOString().split("T")[0], + LineItems: [{ + Description: 'Services', + Quantity: 2, + UnitAmount: 230, + AccountCode: '400' + }] + })); + } + + currentApp.core.invoices.saveInvoices(invoices) + .then(function(response) { + expect(response.entities).to.have.length.greaterThan(9); + done(); + }) + .catch(function(err) { + done(wrapError(err)); + }) + + }); }); describe('payments', function() { @@ -1433,6 +1595,11 @@ describe.skip('regression tests', function() { function wrapError(err) { if (err instanceof Error) return err; - else if (err.statusCode) - return new Error(err.statusCode + ': ' + err.exception.Message); + else if (err.statusCode) { + var msg = err.data; + if (err.exception && err.exception.Message) { + msg = err.exception.Message; + } + return new Error(err.statusCode + ': ' + msg); + } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d97ca20d..ce4ed3d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -662,6 +662,10 @@ esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" +events@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + eventsource@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-0.1.6.tgz#0acede849ed7dd1ccc32c811bb11b944d4f29232" @@ -787,6 +791,12 @@ form-data@~2.1.1: combined-stream "^1.0.5" mime-types "^2.1.12" +formatio@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" + dependencies: + samsam "~1.1" + fresh@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.2.0.tgz#bfd9402cf3df12c4a4c310c79f99a3dde13d34a7" @@ -969,6 +979,10 @@ inherits@2, inherits@~2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + invariant@^2.2.0: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" @@ -1292,6 +1306,10 @@ log4js@~0.6.9: readable-stream "~1.0.2" semver "~4.3.3" +lolex@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -1871,6 +1889,10 @@ rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.4: dependencies: glob "^7.0.5" +samsam@1.1.2, samsam@~1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" + sax@>=0.6.0, sax@^1.1.4: version "1.2.2" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.2.tgz#fd8631a23bc7826bef5d871bdb87378c95647828" @@ -1904,6 +1926,15 @@ signal-exit@^3.0.0, signal-exit@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" +sinon@^1.17.7: + version "1.17.7" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" + dependencies: + formatio "1.1.1" + lolex "1.3.2" + samsam "1.1.2" + util ">=0.10.3 <1" + slide@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" @@ -2163,6 +2194,12 @@ url-parse@1.0.x: querystringify "0.0.x" requires-port "1.0.x" +"util@>=0.10.3 <1": + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + uuid@^3.0.0, uuid@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"