├── .nvmrc ├── .node-version ├── .npmrc ├── web_deploy ├── swagger-ui │ ├── css │ │ ├── typography.css │ │ ├── reset.css │ │ └── style.css │ ├── images │ │ ├── expand.gif │ │ ├── collapse.gif │ │ ├── favicon.ico │ │ ├── throbber.gif │ │ ├── logo_small.png │ │ ├── wordnik_api.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── pet_store_api.png │ │ └── explorer_icons.png │ ├── fonts │ │ ├── DroidSans.ttf │ │ └── DroidSans-Bold.ttf │ ├── lib │ │ ├── highlight.9.1.0.pack_extended.js │ │ ├── object-assign-pollyfill.js │ │ ├── jquery.slideto.min.js │ │ ├── jquery.wiggle.min.js │ │ ├── jquery.ba-bbq.min.js │ │ ├── swagger-oauth.js │ │ ├── highlight.9.1.0.pack.js │ │ └── marked.js │ ├── o2c.html │ ├── lang │ │ ├── translator.js │ │ ├── ko-kr.js │ │ ├── zh-cn.js │ │ ├── ja.js │ │ ├── tr.js │ │ ├── pl.js │ │ ├── pt.js │ │ ├── en.js │ │ ├── ru.js │ │ ├── ca.js │ │ ├── geo.js │ │ ├── it.js │ │ ├── es.js │ │ ├── fr.js │ │ └── el.js │ └── index.html ├── logo.png ├── index.html └── swagger.yml ├── .dockerignore ├── web ├── logo.png └── index.html ├── deploy-docs.sh ├── .yo-rc.json ├── Dockerfile ├── lib ├── handlers │ ├── getBooks.js │ ├── getAccounts.js │ ├── postMarkToMarket.js │ ├── getBalance.js │ ├── getTransactions.js │ ├── postBooks.js │ ├── getBookEntries.js │ └── postBookEntry.js ├── errors.js └── server.js ├── spec ├── code_samples │ └── README.md ├── README.md └── swagger.yaml ├── scripts ├── deploy-branch.js └── build.js ├── index.js ├── test ├── 0_setup_test.js ├── testDB.js ├── 2_forex_test.js ├── 1_test.js └── 3_api_test.js ├── server.js ├── models ├── types.js ├── connection.js ├── transaction.js ├── journal.js └── book.js ├── .gitignore ├── gulpfile.js ├── package.json ├── README.md └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 7.10.1 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save=true 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/css/typography.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | *iml 4 | *.log 5 | -------------------------------------------------------------------------------- /web/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web/logo.png -------------------------------------------------------------------------------- /web_deploy/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/logo.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/expand.gif -------------------------------------------------------------------------------- /web_deploy/swagger-ui/fonts/DroidSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/fonts/DroidSans.ttf -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/collapse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/collapse.gif -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/favicon.ico -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/throbber.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/throbber.gif -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/logo_small.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/wordnik_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/wordnik_api.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/fonts/DroidSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/fonts/DroidSans-Bold.ttf -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/favicon-16x16.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/favicon-32x32.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/pet_store_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/pet_store_api.png -------------------------------------------------------------------------------- /web_deploy/swagger-ui/images/explorer_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CjS77/ale/HEAD/web_deploy/swagger-ui/images/explorer_icons.png -------------------------------------------------------------------------------- /deploy-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | git add web_deploy 3 | git commit -m "Update Swagger Documentation" 4 | git subtree push --prefix web_deploy origin gh-pages 5 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-openapi-repo": { 3 | "importExistingSpec": false, 4 | "name": "ale", 5 | "repo": "CjS77/ale", 6 | "splitSpec": false, 7 | "samples": true, 8 | "installSwaggerUI": true 9 | } 10 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:9 2 | MAINTAINER "Cayle Sharrock " 3 | EXPOSE 8813 4 | ENV NODE_ENV=production 5 | USER node 6 | 7 | RUN mkdir /home/node/app 8 | WORKDIR /home/node/app 9 | ADD --chown=node:node . ./ 10 | RUN yarn 11 | RUN yarn add pg pg-hstore 12 | 13 | CMD ["node", "/home/node/app/server.js"] 14 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/highlight.9.1.0.pack_extended.js: -------------------------------------------------------------------------------- 1 | "use strict";!function(){var h,l;h=hljs.configure,hljs.configure=function(l){var i=l.highlightSizeThreshold;hljs.highlightSizeThreshold=i===+i?i:null,h.call(this,l)},l=hljs.highlightBlock,hljs.highlightBlock=function(h){var i=h.innerHTML,g=hljs.highlightSizeThreshold;(null==g||g>i.length)&&l.call(hljs,h)}}(); -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/object-assign-pollyfill.js: -------------------------------------------------------------------------------- 1 | "function"!=typeof Object.assign&&!function(){Object.assign=function(n){"use strict";if(void 0===n||null===n)throw new TypeError("Cannot convert undefined or null to object");for(var t=Object(n),o=1;o { 12 | res.json(books.map(b => b.values())); 13 | }).catch(err => { 14 | next(err); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /spec/code_samples/README.md: -------------------------------------------------------------------------------- 1 | Code samples 2 | ===== 3 | 4 | Generate [x-code-samples](https://github.com/Rebilly/ReDoc/blob/master/docs/redoc-vendor-extensions.md#x-code-samples) 5 | Path `//.` where: 6 | * `` - name of the language from [this](https://github.com/github/linguist/blob/master/lib/linguist/popular.yml) list. 7 | * `` - path of target method, where all slashes replaced with `@` sign. 8 | * `` - verb of target method. 9 | * `` - ignored. 10 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/jquery.wiggle.min.js: -------------------------------------------------------------------------------- 1 | jQuery.fn.wiggle=function(e){var a={speed:50,wiggles:3,travel:5,callback:null},e=jQuery.extend(a,e);return this.each(function(){var a=this,l=(jQuery(this).wrap('
').css("position","relative"),0);for(i=1;i<=e.wiggles;i++)jQuery(this).animate({left:"-="+e.travel},e.speed).animate({left:"+="+2*e.travel},2*e.speed).animate({left:"-="+e.travel},e.speed,function(){l++,jQuery(a).parent().hasClass("wiggle-wrap")&&jQuery(a).parent().replaceWith(a),l==e.wiggles&&jQuery.isFunction(e.callback)&&e.callback()})})}; -------------------------------------------------------------------------------- /scripts/deploy-branch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | require('shelljs/global'); 4 | const path = require('path'); 5 | 6 | set('-e'); 7 | set('-v'); 8 | 9 | const branch = process.env.TRAVIS_BRANCH && process.env.TRAVIS_BRANCH.toLowerCase(); 10 | if (branch && branch !== 'gh-pages') { 11 | const branchPath = path.join('.tmp', 'preview', branch, '/'); 12 | mkdir('-p', branchPath); 13 | exec('npm run swagger bundle -- -o ' + branchPath + 'swagger.json'); 14 | cp('web/index.html', branchPath); 15 | exec('deploy-to-gh-pages --update .tmp'); 16 | } 17 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/o2c.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const Path = require('path'); 4 | 5 | require('shelljs/global'); 6 | set('-e'); 7 | 8 | mkdir('-p', 'web_deploy'); 9 | 10 | cp('-R', 'web/*', 'web_deploy/'); 11 | 12 | exec('npm run swagger bundle -- -o web_deploy/swagger.json'); 13 | exec('npm run swagger bundle -- --yaml -o web_deploy/swagger.yml'); 14 | 15 | const SWAGGER_UI_DIST = Path.dirname(require.resolve('swagger-ui')); 16 | rm('-rf', 'web_deploy/swagger-ui/'); 17 | cp('-R', SWAGGER_UI_DIST, 'web_deploy/swagger-ui/'); 18 | sed('-i', 'http://petstore.swagger.io/v2/swagger.json', '../swagger.json', 'web_deploy/swagger-ui/index.html') 19 | 20 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ale API Specification 5 | 6 | 7 | 8 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /web_deploy/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ale API Specification 5 | 6 | 7 | 8 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/handlers/getAccounts.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List all accounts 3 | * 4 | * GET: /books/{bookId}/accounts 5 | * 6 | */ 7 | const Book = require('../../models/book'); 8 | const {AleError, codes} = require('../errors'); 9 | exports.handler = function getAccounts(req, res, next) { 10 | let id = parseInt(req.params.bookId); 11 | Book.findById(id).then(book => { 12 | if (!book) { 13 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 14 | } 15 | return book.listAccounts(); 16 | }).then(accounts => { 17 | res.json(accounts); 18 | }).catch(err => { 19 | return next(err); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/css/reset.css: -------------------------------------------------------------------------------- 1 | a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:after,blockquote:before,q:after,q:before{content:'';content:none}table{border-collapse:collapse;border-spacing:0} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | module.exports = { 16 | Book: require('./models/book'), 17 | Transaction: require('./models/transaction'), 18 | JournalEntry: require('./models/journal'), 19 | }; 20 | -------------------------------------------------------------------------------- /lib/handlers/postMarkToMarket.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mark the account(s) to market 3 | * 4 | * POST: /books/{bookId}/marktomarket 5 | * 6 | * body: 7 | * accounts {array} 8 | * exchangeRates {object} 9 | * 10 | */ 11 | const Book = require('../../models/book'); 12 | const {AleError, codes} = require('../errors'); 13 | exports.handler = function postMarketToMarket(req, res, next) { 14 | let id = parseInt(req.params.bookId); 15 | Book.findById(id).then(book => { 16 | if (!book) { 17 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 18 | } 19 | return book.markToMarket({ account: req.body.accounts }, req.body.exchangeRates); 20 | }).then(profit => { 21 | res.json(profit); 22 | }).catch(err => { 23 | return next(err); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /test/0_setup_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2018 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | const testDB = require('./testDB'); 17 | 18 | describe('Create the database.', () => { 19 | before(done => { 20 | testDB.create(done); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | ## Global headers 2 | 3 | In order to minimize duplications you can use `headers` global object (similar to `definitions`, `responses`). 4 | During build process all references to global `headers` will be inlined and `headers` will be removed form resulting spec so spec will be valid (global `headers` is not allowed by Swagger spec): 5 | 6 | Example: 7 | ```yaml 8 | ... 9 | headers: 10 | Rate-Limit-Limit: 11 | description: The number of allowed requests in the current period 12 | type: integer 13 | ... 14 | paths: 15 | /api-keys: 16 | get: 17 | summary: Retrieve a list of api keys 18 | responses: 19 | 200: 20 | description: A list of api keys was retrieved successfully 21 | headers: 22 | Rate-Limit-Limit: 23 | $ref: "#/headers/Rate-Limit-Limit" 24 | ``` 25 | -------------------------------------------------------------------------------- /lib/handlers/getBalance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return an account balance 3 | * 4 | * GET: /books/{bookId}/balance 5 | * 6 | * query: 7 | * account {string} The account to get the balance for. 8 | * inQuoteCurrency {boolean} If true (default), converts all values to the quote currency first. 9 | * 10 | */ 11 | const Book = require('../../models/book'); 12 | const {AleError, codes} = require('../errors'); 13 | 14 | exports.handler = function getBalance(req, res, next) { 15 | let id = parseInt(req.params.bookId); 16 | Book.findById(id).then(book => { 17 | if (!book) { 18 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 19 | } 20 | const { account, perPage, page } = req.query; 21 | const inQuote = req.query.inQuoteCurrency !== false; 22 | return book.getBalance({ account, perPage, page }, inQuote); 23 | }).then(balance => { 24 | res.json(balance); 25 | }).catch(err => { 26 | return next(err); 27 | }); 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2018 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | const app = require('./lib/server'); 17 | const port = process.env.ALE_PORT || 8813; 18 | 19 | app.listen(port, (err) => { 20 | if (err) { 21 | console.error(err); 22 | process.exit(1); 23 | } 24 | console.log(`Listening on port ${port}`); 25 | }); 26 | 27 | process.on('SIGTERM', () => { 28 | console.log('Shutdown signal received. Bye'); 29 | process.exit(0); 30 | }); 31 | -------------------------------------------------------------------------------- /lib/handlers/getTransactions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List all transactions for given accounts 3 | * 4 | * GET: /books/{bookId}/transactions 5 | * 6 | * query: 7 | * accounts {string} A comma-separated search term for accounts. 8 | * perPage {integer} The number of results per page. 9 | * page {integer} The page number. 10 | * 11 | */ 12 | const Book = require('../../models/book'); 13 | const {AleError, codes} = require('../errors'); 14 | exports.handler = function getTransactions(req, res, next) { 15 | let id = parseInt(req.params.bookId); 16 | Book.findById(id).then(book => { 17 | if (!book) { 18 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 19 | } 20 | const account = req.query.accounts.split(','); 21 | const { perPage, page } = req.query; 22 | return book.getTransactions({ account, perPage, page }); 23 | }).then(txs => { 24 | const result = txs.map(t => t.values()); 25 | res.json(result); 26 | }).catch(err => { 27 | return next(err); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /models/types.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const BigNumber = require('bignumber.js'); 16 | const Sequelize = require('sequelize'); 17 | const PRECISION = 16; 18 | const ZERO = new BigNumber(0); 19 | const NEAR_ZERO = 1e-10; 20 | const CURRENCY_LARGE = Sequelize.DECIMAL(40, PRECISION); 21 | const CURRENCY_CODE = Sequelize.STRING(3); 22 | 23 | module.exports = { 24 | PRECISION, 25 | ZERO, 26 | NEAR_ZERO, 27 | CURRENCY_LARGE, 28 | CURRENCY_CODE 29 | }; 30 | -------------------------------------------------------------------------------- /models/connection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const Sequelize = require('sequelize'); 16 | const conn = process.env.ALE_CONNECTION; 17 | const { AleError, codes } = require('../lib/errors'); 18 | 19 | if (!conn) { 20 | throw new AleError('ALE_CONNECTION envar is not set, cannot connect to database', codes.DatabaseConnectionError); 21 | } 22 | 23 | const options = process.env.ALE_DEBUG === 'true' ? { logging: console.log } : { logging: null }; 24 | const sequelize = new Sequelize(conn, options); 25 | 26 | module.exports = sequelize; 27 | -------------------------------------------------------------------------------- /lib/handlers/postBooks.js: -------------------------------------------------------------------------------- 1 | const Book = require('../../models/book'); 2 | const { AleError, codes } = require('../errors'); 3 | /** 4 | * postBooks 5 | * 6 | * POST: /books/ 7 | * 8 | * body: 9 | * id {integer} The id for the book the book. 10 | * name {string} The name of the book. 11 | * currency {string} The currency the book is referenced in. 12 | * createdAt {number} The timestamp of when the book was created. 13 | * updatedAt {number} The timestamp of the last time this entry was modified. 14 | * 15 | */ 16 | exports.handler = function postBooks(req, res, next) { 17 | const { name, currency } = req.body || {}; 18 | if (!name || !currency) { 19 | const err = new AleError('Missing name or currency', codes.MissingInput); 20 | return next(err); 21 | } 22 | Book.getOrCreateBook(name, currency).then(result => { 23 | const obj = Object.assign({ 24 | success: result.isNew, 25 | message: result.isNew ? `Book ${name} (${currency}) created` : `Book ${name} already exists` 26 | }, result.book.values()); 27 | res.json(obj); 28 | }).catch(err => { 29 | return next(err); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | ### JetBrains template 61 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 62 | .idea 63 | *.iml 64 | out/ 65 | 66 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const util = require('gulp-util') 3 | const gulpConnect = require('gulp-connect'); 4 | const connect = require('connect'); 5 | const cors = require('cors'); 6 | const path = require('path'); 7 | const exec = require('child_process').exec; 8 | const portfinder = require('portfinder'); 9 | const swaggerRepo = require('swagger-repo'); 10 | 11 | const DIST_DIR = 'web_deploy'; 12 | 13 | gulp.task('serve', ['build', 'watch', 'edit'], function() { 14 | portfinder.getPort({port: 3000}, function (err, port) { 15 | gulpConnect.server({ 16 | root: [DIST_DIR], 17 | livereload: true, 18 | port: port, 19 | middleware: function (gulpConnect, opt) { 20 | return [ 21 | cors() 22 | ] 23 | } 24 | }); 25 | }); 26 | }); 27 | 28 | gulp.task('edit', function() { 29 | portfinder.getPort({port: 5000}, function (err, port) { 30 | var app = connect(); 31 | app.use(swaggerRepo.swaggerEditorMiddleware()); 32 | app.listen(port); 33 | util.log(util.colors.green('swagger-editor started http://localhost:' + port)); 34 | }); 35 | }); 36 | 37 | gulp.task('build', function (cb) { 38 | exec('npm run build', function (err, stdout, stderr) { 39 | console.log(stderr); 40 | cb(err); 41 | }); 42 | }); 43 | 44 | gulp.task('reload', ['build'], function () { 45 | gulp.src(DIST_DIR).pipe(gulpConnect.reload()) 46 | }); 47 | 48 | gulp.task('watch', function () { 49 | gulp.watch(['spec/**/*', 'web/**/*'], ['reload']); 50 | }); 51 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/translator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Translator for documentation pages. 5 | * 6 | * To enable translation you should include one of language-files in your index.html 7 | * after . 8 | * For example - 9 | * 10 | * If you wish to translate some new texts you should do two things: 11 | * 1. Add a new phrase pair ("New Phrase": "New Translation") into your language file (for example lang/ru.js). It will be great if you add it in other language files too. 12 | * 2. Mark that text it templates this way New Phrase or . 13 | * The main thing here is attribute data-sw-translate. Only inner html, title-attribute and value-attribute are going to translate. 14 | * 15 | */ 16 | window.SwaggerTranslator = { 17 | 18 | _words:[], 19 | 20 | translate: function(sel) { 21 | var $this = this; 22 | sel = sel || '[data-sw-translate]'; 23 | 24 | $(sel).each(function() { 25 | $(this).html($this._tryTranslate($(this).html())); 26 | 27 | $(this).val($this._tryTranslate($(this).val())); 28 | $(this).attr('title', $this._tryTranslate($(this).attr('title'))); 29 | }); 30 | }, 31 | 32 | _tryTranslate: function(word) { 33 | return this._words[$.trim(word)] !== undefined ? this._words[$.trim(word)] : word; 34 | }, 35 | 36 | learn: function(wordsMap) { 37 | this._words = wordsMap; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a.l.e", 3 | "version": "1.0.0", 4 | "description": "Double accounting package based on node.js + sequelize", 5 | "main": "index.js", 6 | "repository": "git@github.com:CjS77/ale.git", 7 | "author": "Cayle Sharrock ", 8 | "license": "Apache-2.0", 9 | "scripts": { 10 | "test": "NODE_ENV=test mocha test/*.js", 11 | "deploy": "npm run build && ./deploy-docs.sh", 12 | "build": "node ./scripts/build.js", 13 | "swagger": "swagger-repo", 14 | "test:swagger": "swagger-repo validate", 15 | "start:dev": "gulp serve", 16 | "start": "node server", 17 | "deploy-branch": "node ./scripts/deploy-branch.js" 18 | }, 19 | "keywords": [ 20 | "double-entry", 21 | "accounting", 22 | "account", 23 | "finance", 24 | "sequelize", 25 | "postgres", 26 | "sqlite", 27 | "mysql", 28 | "forex", 29 | "foreign-exchange", 30 | "exchange-rates" 31 | ], 32 | "dependencies": { 33 | "async": "^2.5.0", 34 | "bignumber.js": "^4.0.4", 35 | "body-parser": "1.18.2", 36 | "bower": "^1.7.7", 37 | "connect": "^3.4.1", 38 | "cors": "^2.7.1", 39 | "deploy-to-gh-pages": "^1.1.0", 40 | "express": "4.16.2", 41 | "gulp": "^3.9.1", 42 | "gulp-connect": "^4.2.0", 43 | "gulp-util": "^3.0.7", 44 | "portfinder": "^1.0.3", 45 | "sequelize": "^4.11.0", 46 | "shelljs": "^0.7.0", 47 | "swagger-repo": "^1.0.0", 48 | "swagger-routes": "1.7.0", 49 | "swagger-ui": "^2.1.4" 50 | }, 51 | "devDependencies": { 52 | "mocha": "^3.5.3", 53 | "pg": "^7.3.0", 54 | "pg-hstore": "^2.3.2", 55 | "supertest": "3.0.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2018 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | class AleError extends Error { 17 | constructor(msg, code) { 18 | super(msg); 19 | // Saving class name in the property of our custom error as a shortcut. 20 | this.name = this.constructor.name; 21 | // Capturing stack trace, excluding constructor call from it. 22 | Error.captureStackTrace(this, this.constructor); 23 | this.code = code; 24 | } 25 | 26 | asResponse() { 27 | return { 28 | success: false, 29 | message: this.message, 30 | errorCode: this.code 31 | } 32 | } 33 | } 34 | 35 | const codes = { 36 | UnknownError: -1, 37 | ValidationError: 10, 38 | MismatchedCurrency: 100, 39 | TransactionIDNotFound: 200, 40 | ExchangeRateNotFound: 210, 41 | DatabaseConnectionError: 300, 42 | DatabaseUpdateError: 310, 43 | DatabaseQueryError: 320, 44 | EntryNotBalanced: 400, 45 | MissingInput: 500, 46 | BookDoesNotExist: 510 47 | }; 48 | 49 | module.exports = { 50 | AleError, codes 51 | }; 52 | -------------------------------------------------------------------------------- /lib/handlers/getBookEntries.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fetch the ledger 3 | * 4 | * GET: /books/{bookId}/ledger 5 | * 6 | * path: 7 | * bookId {integer} The book to extract entries from. 8 | * 9 | * query: 10 | * startDate {string} The start date for entries. 11 | * endDate {string} The end date for entries. 12 | * perPage {integer} The number of results per page. 13 | * page {integer} The page number. 14 | * 15 | */ 16 | const Book = require('../../models/book'); 17 | const { AleError, codes } = require('../errors'); 18 | exports.handler = function getBookEntries(req, res, next) { 19 | let startDate; 20 | let endDate; 21 | let id = parseInt(req.params.bookId); 22 | let bookInfo; 23 | Book.findById(id).then(book => { 24 | if (!book) { 25 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 26 | } 27 | if (req.query.startDate) { 28 | startDate = new Date(req.query.startDate); 29 | } 30 | if (req.query.endDate) { 31 | endDate = new Date(req.query.endDate); 32 | } 33 | bookInfo = book.values(); 34 | return book.getLedger({startDate, endDate, page: req.query.page, perPage: req.query.perPage}); 35 | }).then(ledger => { 36 | const result = { 37 | book: bookInfo, 38 | startDate: startDate && startDate.valueOf(), 39 | endDate: endDate && endDate.valueOf(), 40 | }; 41 | result.entries = ledger.map(e => { 42 | const entry = e.values(); 43 | entry.transactions = e.transactions.map(tx => tx.values()); 44 | return entry; 45 | }); 46 | return res.json(result); 47 | }).catch(err => { 48 | return next(err); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2018 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | const swaggerRoutes = require('swagger-routes'); 17 | const express = require('express'); 18 | const app = express(); 19 | const bodyParser = require('body-parser'); 20 | const {codes} = require('./errors'); 21 | 22 | app.use(bodyParser.json()); 23 | 24 | swaggerRoutes(app, { 25 | api: `${__dirname}/../spec/swagger.yaml`, 26 | handlers: { 27 | path: `${__dirname}/handlers`, 28 | generate: process.env.NODE_ENV !== 'production' 29 | }, 30 | authorizers: `${__dirname}/handlers/security`, 31 | maintainHeaders: process.env.NODE_ENV !== 'production' 32 | }); 33 | 34 | // Override error response 35 | app.use((err, req, res, next) => { 36 | if (!err) { 37 | next(); 38 | } 39 | let response = { 40 | success: false, 41 | message: err.message, 42 | errorCode: codes.UnknownError 43 | }; 44 | if (err.constructor.name === 'ValidationError') { 45 | response.errorCode = codes.ValidationError; 46 | } 47 | if (err.asResponse) { 48 | response = err.asResponse() 49 | } 50 | res.status(400).json(response); 51 | }); 52 | 53 | module.exports = app; 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/ko-kr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"경고:폐기예정됨", 6 | "Implementation Notes":"구현 노트", 7 | "Response Class":"응답 클래스", 8 | "Status":"상태", 9 | "Parameters":"매개변수들", 10 | "Parameter":"매개변수", 11 | "Value":"값", 12 | "Description":"설명", 13 | "Parameter Type":"매개변수 타입", 14 | "Data Type":"데이터 타입", 15 | "Response Messages":"응답 메세지", 16 | "HTTP Status Code":"HTTP 상태 코드", 17 | "Reason":"원인", 18 | "Response Model":"응답 모델", 19 | "Request URL":"요청 URL", 20 | "Response Body":"응답 본문", 21 | "Response Code":"응답 코드", 22 | "Response Headers":"응답 헤더", 23 | "Hide Response":"응답 숨기기", 24 | "Headers":"헤더", 25 | "Try it out!":"써보기!", 26 | "Show/Hide":"보이기/숨기기", 27 | "List Operations":"목록 작업", 28 | "Expand Operations":"전개 작업", 29 | "Raw":"원본", 30 | "can't parse JSON. Raw result":"JSON을 파싱할수 없음. 원본결과:", 31 | "Model Schema":"모델 스키마", 32 | "Model":"모델", 33 | "apply":"적용", 34 | "Username":"사용자 이름", 35 | "Password":"암호", 36 | "Terms of service":"이용약관", 37 | "Created by":"작성자", 38 | "See more at":"추가정보:", 39 | "Contact the developer":"개발자에게 문의", 40 | "api version":"api버전", 41 | "Response Content Type":"응답Content Type", 42 | "fetching resource":"리소스 가져오기", 43 | "fetching resource list":"리소스 목록 가져오기", 44 | "Explore":"탐색", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore 예제 보기", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"서버로부터 읽어들일수 없습니다. access-control-origin 설정이 올바르지 않을수 있습니다.", 47 | "Please specify the protocol for":"다음을 위한 프로토콜을 정하세요", 48 | "Can't read swagger JSON from":"swagger JSON 을 다음으로 부터 읽을수 없습니다", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"리소스 정보 불러오기 완료. Swagger UI 랜더링", 50 | "Unable to read api":"api를 읽을 수 없습니다.", 51 | "from path":"다음 경로로 부터", 52 | "server returned":"서버 응답함." 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/zh-cn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告:已过时", 6 | "Implementation Notes":"实现备注", 7 | "Response Class":"响应类", 8 | "Status":"状态", 9 | "Parameters":"参数", 10 | "Parameter":"参数", 11 | "Value":"值", 12 | "Description":"描述", 13 | "Parameter Type":"参数类型", 14 | "Data Type":"数据类型", 15 | "Response Messages":"响应消息", 16 | "HTTP Status Code":"HTTP状态码", 17 | "Reason":"原因", 18 | "Response Model":"响应模型", 19 | "Request URL":"请求URL", 20 | "Response Body":"响应体", 21 | "Response Code":"响应码", 22 | "Response Headers":"响应头", 23 | "Hide Response":"隐藏响应", 24 | "Headers":"头", 25 | "Try it out!":"试一下!", 26 | "Show/Hide":"显示/隐藏", 27 | "List Operations":"显示操作", 28 | "Expand Operations":"展开操作", 29 | "Raw":"原始", 30 | "can't parse JSON. Raw result":"无法解析JSON. 原始结果", 31 | "Example Value":"示例", 32 | "Click to set as parameter value":"点击设置参数", 33 | "Model Schema":"模型架构", 34 | "Model":"模型", 35 | "apply":"应用", 36 | "Username":"用户名", 37 | "Password":"密码", 38 | "Terms of service":"服务条款", 39 | "Created by":"创建者", 40 | "See more at":"查看更多:", 41 | "Contact the developer":"联系开发者", 42 | "api version":"api版本", 43 | "Response Content Type":"响应Content Type", 44 | "Parameter content type:":"参数类型:", 45 | "fetching resource":"正在获取资源", 46 | "fetching resource list":"正在获取资源列表", 47 | "Explore":"浏览", 48 | "Show Swagger Petstore Example Apis":"显示 Swagger Petstore 示例 Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"无法从服务器读取。可能没有正确设置access-control-origin。", 50 | "Please specify the protocol for":"请指定协议:", 51 | "Can't read swagger JSON from":"无法读取swagger JSON于", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"已加载资源信息。正在渲染Swagger UI", 53 | "Unable to read api":"无法读取api", 54 | "from path":"从路径", 55 | "server returned":"服务器返回" 56 | }); 57 | -------------------------------------------------------------------------------- /lib/handlers/postBookEntry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Post a new Journal entry 3 | * 4 | * POST: /books/{bookId}/ledger 5 | * 6 | * body: 7 | * memo {string} The Journal Entry description. 8 | * timestamp {string} The time stamp for the journal entry. 9 | * transactions {array} 10 | * 11 | */ 12 | const {codes, AleError} = require('../errors'); 13 | const Book = require('../../models/book'); 14 | 15 | exports.handler = function postBookEntry(req, res, next) { 16 | let id = parseInt(req.params.bookId); 17 | let bookInfo; 18 | Book.findById(id).then(book => { 19 | if (!book) { 20 | throw new AleError(`Book with id ${id} does not exist`, codes.BookDoesNotExist); 21 | } 22 | bookInfo = book.values(); 23 | const candidateEntry = req.body; 24 | const ts = new Date(candidateEntry.timestamp); 25 | if (isNaN(ts.valueOf())) { 26 | throw new AleError(`Invalid Journal entry timestamp: ${candidateEntry.timestamp}`, codes.ValidationError); 27 | } 28 | const newEntry = book.newJournalEntry(candidateEntry.memo, ts); 29 | candidateEntry.transactions.forEach((tx, i) => { 30 | const amount = parseFloat(tx.credit || 0) - parseFloat(tx.debit || 0); 31 | if (!isFinite(amount)) { 32 | throw new AleError(`Invalid credit and/or debit amount for transaction ${i}`, codes.ValidationError); 33 | } 34 | const isCredit = amount > 0; 35 | const absAmount = Math.abs(amount); 36 | const account = tx.account; 37 | const exchangeRate = parseFloat(tx.exchangeRate) || 1.0; 38 | const currency = tx.currency || bookInfo.currency; 39 | newEntry.newTransaction(account, absAmount, isCredit, currency, exchangeRate); 40 | }); 41 | return newEntry.commit(); 42 | }).then(e => { 43 | return res.json({ 44 | success: true, 45 | message: 'Journal Entry has been saved', 46 | id: e.id 47 | }); 48 | }).catch(err => { 49 | return next(err); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /test/testDB.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const { Client } = require('pg'); 16 | const async = require('async'); 17 | const Transaction = require('../models/transaction'); 18 | const Book = require('../models/book'); 19 | const JournalEntry = require('../models/journal'); 20 | 21 | process.env.ALE_CONNECTION = process.env.ALE_CONNECTION || 'postgres://postgres@localhost/ale-test'; 22 | const masterUri = process.env.MASTER_ALE_CONNECTION || 'postgres://postgres@localhost/postgres'; 23 | 24 | let sequelize = require('../models/connection'); 25 | 26 | module.exports.create = (done) => { 27 | create(masterUri, 'ale-test', done); 28 | }; 29 | 30 | module.exports.destroy = () => { 31 | return sequelize.drop().then(() => { 32 | return sequelize.close(); 33 | }); 34 | }; 35 | 36 | module.exports.clear = () => { 37 | return Transaction.truncate().then(() => { 38 | return JournalEntry.truncate({ cascade: true }); 39 | }).then(() => { 40 | return Book.truncate({ cascade: true }); 41 | }) 42 | }; 43 | 44 | function create(master_uri, db_name, cb) { 45 | const queries = [ 46 | `DROP DATABASE IF EXISTS "${db_name}";`, 47 | `CREATE DATABASE "${db_name}";`, 48 | `ALTER DATABASE "${db_name}" SET TIMEZONE TO 'UTC';` 49 | ]; 50 | const client = new Client({ connectionString: master_uri }); 51 | client.connect(); 52 | async.eachSeries(queries, function(sql, done) { 53 | client.query(sql, done); 54 | }, cb); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/ja.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"警告: 廃止予定", 6 | "Implementation Notes":"実装メモ", 7 | "Response Class":"レスポンスクラス", 8 | "Status":"ステータス", 9 | "Parameters":"パラメータ群", 10 | "Parameter":"パラメータ", 11 | "Value":"値", 12 | "Description":"説明", 13 | "Parameter Type":"パラメータタイプ", 14 | "Data Type":"データタイプ", 15 | "Response Messages":"レスポンスメッセージ", 16 | "HTTP Status Code":"HTTPステータスコード", 17 | "Reason":"理由", 18 | "Response Model":"レスポンスモデル", 19 | "Request URL":"リクエストURL", 20 | "Response Body":"レスポンスボディ", 21 | "Response Code":"レスポンスコード", 22 | "Response Headers":"レスポンスヘッダ", 23 | "Hide Response":"レスポンスを隠す", 24 | "Headers":"ヘッダ", 25 | "Try it out!":"実際に実行!", 26 | "Show/Hide":"表示/非表示", 27 | "List Operations":"操作一覧", 28 | "Expand Operations":"操作の展開", 29 | "Raw":"未加工", 30 | "can't parse JSON. Raw result":"JSONへ解釈できません. 未加工の結果", 31 | "Example Value":"値の例", 32 | "Model Schema":"モデルスキーマ", 33 | "Model":"モデル", 34 | "Click to set as parameter value":"パラメータ値と設定するにはクリック", 35 | "apply":"実行", 36 | "Username":"ユーザ名", 37 | "Password":"パスワード", 38 | "Terms of service":"サービス利用規約", 39 | "Created by":"Created by", 40 | "See more at":"詳細を見る", 41 | "Contact the developer":"開発者に連絡", 42 | "api version":"APIバージョン", 43 | "Response Content Type":"レスポンス コンテンツタイプ", 44 | "Parameter content type:":"パラメータコンテンツタイプ:", 45 | "fetching resource":"リソースの取得", 46 | "fetching resource list":"リソース一覧の取得", 47 | "Explore":"調査", 48 | "Show Swagger Petstore Example Apis":"SwaggerペットストアAPIの表示", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"サーバから読み込めません. 適切なaccess-control-origin設定を持っていない可能性があります.", 50 | "Please specify the protocol for":"プロトコルを指定してください", 51 | "Can't read swagger JSON from":"次からswagger JSONを読み込めません", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"リソース情報の読み込みが完了しました. Swagger UIを描画しています", 53 | "Unable to read api":"APIを読み込めません", 54 | "from path":"次のパスから", 55 | "server returned":"サーバからの返答" 56 | }); 57 | -------------------------------------------------------------------------------- /models/transaction.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const Sequelize = require('sequelize'); 16 | const sequelize = require('./connection'); 17 | const { CURRENCY_LARGE } = require('./types'); 18 | 19 | /** 20 | * A Transaction describes a single entry into an account. A transaction is always associated with exactly one Journal entry; the latter ensure that the transactions are balanced 21 | */ 22 | Transaction = sequelize.define('transaction', { 23 | credit: { type: CURRENCY_LARGE, validate: { isDecimal: true }, defaultValue: 0.0 }, 24 | debit: { type: CURRENCY_LARGE, validate: { isDecimal: true }, defaultValue: 0.0 }, 25 | exchangeRate: { type: CURRENCY_LARGE, validate: { isDecimal: true }, defaultValue: 1.0 }, 26 | currency: { type: Sequelize.STRING, defaultValue: 'USD', notNull: true }, 27 | account: Sequelize.STRING, 28 | timestamp: { type: Sequelize.DATE, validate: { isDate: true }, default: Date.now }, 29 | voided: { type: Sequelize.BOOLEAN, default: false }, 30 | voidReason: Sequelize.STRING, 31 | }, {}); 32 | 33 | /** 34 | * Return instance in native formats 35 | */ 36 | Transaction.prototype.values = function() { 37 | return { 38 | id: this.getDataValue('id'), 39 | credit: +this.getDataValue('credit'), 40 | debit: +this.getDataValue('debit'), 41 | exchange_rate: +this.getDataValue('exchangeRate'), 42 | currency: this.getDataValue('currency'), 43 | account: this.getDataValue('account'), 44 | timestamp: new Date(this.getDataValue('timestamp')), 45 | voided: this.getDataValue('voided'), 46 | voidReason: this.getDataValue('voidReason'), 47 | }; 48 | }; 49 | 50 | // Foreign key relationships are setup in journal.js 51 | module.exports = Transaction; 52 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/tr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uyarı: Deprecated", 6 | "Implementation Notes":"Gerçekleştirim Notları", 7 | "Response Class":"Dönen Sınıf", 8 | "Status":"Statü", 9 | "Parameters":"Parametreler", 10 | "Parameter":"Parametre", 11 | "Value":"Değer", 12 | "Description":"Açıklama", 13 | "Parameter Type":"Parametre Tipi", 14 | "Data Type":"Veri Tipi", 15 | "Response Messages":"Dönüş Mesajı", 16 | "HTTP Status Code":"HTTP Statü Kodu", 17 | "Reason":"Gerekçe", 18 | "Response Model":"Dönüş Modeli", 19 | "Request URL":"İstek URL", 20 | "Response Body":"Dönüş İçeriği", 21 | "Response Code":"Dönüş Kodu", 22 | "Response Headers":"Dönüş Üst Bilgileri", 23 | "Hide Response":"Dönüşü Gizle", 24 | "Headers":"Üst Bilgiler", 25 | "Try it out!":"Dene!", 26 | "Show/Hide":"Göster/Gizle", 27 | "List Operations":"Operasyonları Listele", 28 | "Expand Operations":"Operasyonları Aç", 29 | "Raw":"Ham", 30 | "can't parse JSON. Raw result":"JSON çözümlenemiyor. Ham sonuç", 31 | "Model Schema":"Model Şema", 32 | "Model":"Model", 33 | "apply":"uygula", 34 | "Username":"Kullanıcı Adı", 35 | "Password":"Parola", 36 | "Terms of service":"Servis şartları", 37 | "Created by":"Oluşturan", 38 | "See more at":"Daha fazlası için", 39 | "Contact the developer":"Geliştirici ile İletişime Geçin", 40 | "api version":"api versiyon", 41 | "Response Content Type":"Dönüş İçerik Tipi", 42 | "fetching resource":"kaynak getiriliyor", 43 | "fetching resource list":"kaynak listesi getiriliyor", 44 | "Explore":"Keşfet", 45 | "Show Swagger Petstore Example Apis":"Swagger Petstore Örnek Api'yi Gör", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Sunucudan okuma yapılamıyor. Sunucu access-control-origin ayarlarınızı kontrol edin.", 47 | "Please specify the protocol for":"Lütfen istenen adres için protokol belirtiniz", 48 | "Can't read swagger JSON from":"Swagger JSON bu kaynaktan okunamıyor", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Kaynak baglantısı tamamlandı. Swagger UI gösterime hazırlanıyor", 50 | "Unable to read api":"api okunamadı", 51 | "from path":"yoldan", 52 | "server returned":"sunucuya dönüldü" 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/pl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Uwaga: Wycofane", 6 | "Implementation Notes":"Uwagi Implementacji", 7 | "Response Class":"Klasa Odpowiedzi", 8 | "Status":"Status", 9 | "Parameters":"Parametry", 10 | "Parameter":"Parametr", 11 | "Value":"Wartość", 12 | "Description":"Opis", 13 | "Parameter Type":"Typ Parametru", 14 | "Data Type":"Typ Danych", 15 | "Response Messages":"Wiadomości Odpowiedzi", 16 | "HTTP Status Code":"Kod Statusu HTTP", 17 | "Reason":"Przyczyna", 18 | "Response Model":"Model Odpowiedzi", 19 | "Request URL":"URL Wywołania", 20 | "Response Body":"Treść Odpowiedzi", 21 | "Response Code":"Kod Odpowiedzi", 22 | "Response Headers":"Nagłówki Odpowiedzi", 23 | "Hide Response":"Ukryj Odpowiedź", 24 | "Headers":"Nagłówki", 25 | "Try it out!":"Wypróbuj!", 26 | "Show/Hide":"Pokaż/Ukryj", 27 | "List Operations":"Lista Operacji", 28 | "Expand Operations":"Rozwiń Operacje", 29 | "Raw":"Nieprzetworzone", 30 | "can't parse JSON. Raw result":"nie można przetworzyć pliku JSON. Nieprzetworzone dane", 31 | "Model Schema":"Schemat Modelu", 32 | "Model":"Model", 33 | "apply":"użyj", 34 | "Username":"Nazwa użytkownika", 35 | "Password":"Hasło", 36 | "Terms of service":"Warunki używania", 37 | "Created by":"Utworzone przez", 38 | "See more at":"Zobacz więcej na", 39 | "Contact the developer":"Kontakt z deweloperem", 40 | "api version":"wersja api", 41 | "Response Content Type":"Typ Zasobu Odpowiedzi", 42 | "fetching resource":"ładowanie zasobu", 43 | "fetching resource list":"ładowanie listy zasobów", 44 | "Explore":"Eksploruj", 45 | "Show Swagger Petstore Example Apis":"Pokaż Przykładowe Api Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Brak połączenia z serwerem. Może on nie mieć odpowiednich ustawień access-control-origin.", 47 | "Please specify the protocol for":"Proszę podać protokół dla", 48 | "Can't read swagger JSON from":"Nie można odczytać swagger JSON z", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Ukończono Ładowanie Informacji o Zasobie. Renderowanie Swagger UI", 50 | "Unable to read api":"Nie można odczytać api", 51 | "from path":"ze ścieżki", 52 | "server returned":"serwer zwrócił" 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/pt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Aviso: Depreciado", 6 | "Implementation Notes":"Notas de Implementação", 7 | "Response Class":"Classe de resposta", 8 | "Status":"Status", 9 | "Parameters":"Parâmetros", 10 | "Parameter":"Parâmetro", 11 | "Value":"Valor", 12 | "Description":"Descrição", 13 | "Parameter Type":"Tipo de parâmetro", 14 | "Data Type":"Tipo de dados", 15 | "Response Messages":"Mensagens de resposta", 16 | "HTTP Status Code":"Código de status HTTP", 17 | "Reason":"Razão", 18 | "Response Model":"Modelo resposta", 19 | "Request URL":"URL requisição", 20 | "Response Body":"Corpo da resposta", 21 | "Response Code":"Código da resposta", 22 | "Response Headers":"Cabeçalho da resposta", 23 | "Headers":"Cabeçalhos", 24 | "Hide Response":"Esconder resposta", 25 | "Try it out!":"Tente agora!", 26 | "Show/Hide":"Mostrar/Esconder", 27 | "List Operations":"Listar operações", 28 | "Expand Operations":"Expandir operações", 29 | "Raw":"Cru", 30 | "can't parse JSON. Raw result":"Falha ao analisar JSON. Resulto cru", 31 | "Model Schema":"Modelo esquema", 32 | "Model":"Modelo", 33 | "apply":"Aplicar", 34 | "Username":"Usuário", 35 | "Password":"Senha", 36 | "Terms of service":"Termos do serviço", 37 | "Created by":"Criado por", 38 | "See more at":"Veja mais em", 39 | "Contact the developer":"Contate o desenvolvedor", 40 | "api version":"Versão api", 41 | "Response Content Type":"Tipo de conteúdo da resposta", 42 | "fetching resource":"busca recurso", 43 | "fetching resource list":"buscando lista de recursos", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Não é possível ler do servidor. Pode não ter as apropriadas configurações access-control-origin", 47 | "Please specify the protocol for":"Por favor especifique o protocolo", 48 | "Can't read swagger JSON from":"Não é possível ler o JSON Swagger de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Carregar informação de recurso finalizada. Renderizando Swagger UI", 50 | "Unable to read api":"Não foi possível ler api", 51 | "from path":"do caminho", 52 | "server returned":"servidor retornou" 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/en.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Warning: Deprecated", 6 | "Implementation Notes":"Implementation Notes", 7 | "Response Class":"Response Class", 8 | "Status":"Status", 9 | "Parameters":"Parameters", 10 | "Parameter":"Parameter", 11 | "Value":"Value", 12 | "Description":"Description", 13 | "Parameter Type":"Parameter Type", 14 | "Data Type":"Data Type", 15 | "Response Messages":"Response Messages", 16 | "HTTP Status Code":"HTTP Status Code", 17 | "Reason":"Reason", 18 | "Response Model":"Response Model", 19 | "Request URL":"Request URL", 20 | "Response Body":"Response Body", 21 | "Response Code":"Response Code", 22 | "Response Headers":"Response Headers", 23 | "Hide Response":"Hide Response", 24 | "Headers":"Headers", 25 | "Try it out!":"Try it out!", 26 | "Show/Hide":"Show/Hide", 27 | "List Operations":"List Operations", 28 | "Expand Operations":"Expand Operations", 29 | "Raw":"Raw", 30 | "can't parse JSON. Raw result":"can't parse JSON. Raw result", 31 | "Example Value":"Example Value", 32 | "Model Schema":"Model Schema", 33 | "Model":"Model", 34 | "Click to set as parameter value":"Click to set as parameter value", 35 | "apply":"apply", 36 | "Username":"Username", 37 | "Password":"Password", 38 | "Terms of service":"Terms of service", 39 | "Created by":"Created by", 40 | "See more at":"See more at", 41 | "Contact the developer":"Contact the developer", 42 | "api version":"api version", 43 | "Response Content Type":"Response Content Type", 44 | "Parameter content type:":"Parameter content type:", 45 | "fetching resource":"fetching resource", 46 | "fetching resource list":"fetching resource list", 47 | "Explore":"Explore", 48 | "Show Swagger Petstore Example Apis":"Show Swagger Petstore Example Apis", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Can't read from server. It may not have the appropriate access-control-origin settings.", 50 | "Please specify the protocol for":"Please specify the protocol for", 51 | "Can't read swagger JSON from":"Can't read swagger JSON from", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Finished Loading Resource Information. Rendering Swagger UI", 53 | "Unable to read api":"Unable to read api", 54 | "from path":"from path", 55 | "server returned":"server returned" 56 | }); 57 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/ru.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Предупреждение: Устарело", 6 | "Implementation Notes":"Заметки", 7 | "Response Class":"Пример ответа", 8 | "Status":"Статус", 9 | "Parameters":"Параметры", 10 | "Parameter":"Параметр", 11 | "Value":"Значение", 12 | "Description":"Описание", 13 | "Parameter Type":"Тип параметра", 14 | "Data Type":"Тип данных", 15 | "HTTP Status Code":"HTTP код", 16 | "Reason":"Причина", 17 | "Response Model":"Структура ответа", 18 | "Request URL":"URL запроса", 19 | "Response Body":"Тело ответа", 20 | "Response Code":"HTTP код ответа", 21 | "Response Headers":"Заголовки ответа", 22 | "Hide Response":"Спрятать ответ", 23 | "Headers":"Заголовки", 24 | "Response Messages":"Что может прийти в ответ", 25 | "Try it out!":"Попробовать!", 26 | "Show/Hide":"Показать/Скрыть", 27 | "List Operations":"Операции кратко", 28 | "Expand Operations":"Операции подробно", 29 | "Raw":"В сыром виде", 30 | "can't parse JSON. Raw result":"Не удается распарсить ответ:", 31 | "Example Value":"Пример", 32 | "Model Schema":"Структура", 33 | "Model":"Описание", 34 | "Click to set as parameter value":"Нажмите, чтобы испльзовать в качестве значения параметра", 35 | "apply":"применить", 36 | "Username":"Имя пользователя", 37 | "Password":"Пароль", 38 | "Terms of service":"Условия использования", 39 | "Created by":"Разработано", 40 | "See more at":"Еще тут", 41 | "Contact the developer":"Связаться с разработчиком", 42 | "api version":"Версия API", 43 | "Response Content Type":"Content Type ответа", 44 | "Parameter content type:":"Content Type параметра:", 45 | "fetching resource":"Получение ресурса", 46 | "fetching resource list":"Получение ресурсов", 47 | "Explore":"Показать", 48 | "Show Swagger Petstore Example Apis":"Показать примеры АПИ", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Не удается получить ответ от сервера. Возможно, проблема с настройками доступа", 50 | "Please specify the protocol for":"Пожалуйста, укажите протокол для", 51 | "Can't read swagger JSON from":"Не получается прочитать swagger json из", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Загрузка информации о ресурсах завершена. Рендерим", 53 | "Unable to read api":"Не удалось прочитать api", 54 | "from path":"по адресу", 55 | "server returned":"сервер сказал" 56 | }); 57 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/ca.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertència: Obsolet", 6 | "Implementation Notes":"Notes d'implementació", 7 | "Response Class":"Classe de la Resposta", 8 | "Status":"Estatus", 9 | "Parameters":"Paràmetres", 10 | "Parameter":"Paràmetre", 11 | "Value":"Valor", 12 | "Description":"Descripció", 13 | "Parameter Type":"Tipus del Paràmetre", 14 | "Data Type":"Tipus de la Dada", 15 | "Response Messages":"Missatges de la Resposta", 16 | "HTTP Status Code":"Codi d'Estatus HTTP", 17 | "Reason":"Raó", 18 | "Response Model":"Model de la Resposta", 19 | "Request URL":"URL de la Sol·licitud", 20 | "Response Body":"Cos de la Resposta", 21 | "Response Code":"Codi de la Resposta", 22 | "Response Headers":"Capçaleres de la Resposta", 23 | "Hide Response":"Amagar Resposta", 24 | "Try it out!":"Prova-ho!", 25 | "Show/Hide":"Mostrar/Amagar", 26 | "List Operations":"Llista Operacions", 27 | "Expand Operations":"Expandir Operacions", 28 | "Raw":"Cru", 29 | "can't parse JSON. Raw result":"no puc analitzar el JSON. Resultat cru", 30 | "Example Value":"Valor d'Exemple", 31 | "Model Schema":"Esquema del Model", 32 | "Model":"Model", 33 | "apply":"aplicar", 34 | "Username":"Nom d'usuari", 35 | "Password":"Contrasenya", 36 | "Terms of service":"Termes del servei", 37 | "Created by":"Creat per", 38 | "See more at":"Veure més en", 39 | "Contact the developer":"Contactar amb el desenvolupador", 40 | "api version":"versió de la api", 41 | "Response Content Type":"Tipus de Contingut de la Resposta", 42 | "fetching resource":"recollint recurs", 43 | "fetching resource list":"recollins llista de recursos", 44 | "Explore":"Explorant", 45 | "Show Swagger Petstore Example Apis":"Mostrar API d'Exemple Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No es pot llegir del servidor. Potser no teniu la configuració de control d'accés apropiada.", 47 | "Please specify the protocol for":"Si us plau, especifiqueu el protocol per a", 48 | "Can't read swagger JSON from":"No es pot llegir el JSON de swagger des de", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalitzada la càrrega del recurs informatiu. Renderitzant Swagger UI", 50 | "Unable to read api":"No es pot llegir l'api", 51 | "from path":"des de la ruta", 52 | "server returned":"el servidor ha retornat" 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/geo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"ყურადღება: აღარ გამოიყენება", 6 | "Implementation Notes":"იმპლემენტაციის აღწერა", 7 | "Response Class":"რესპონს კლასი", 8 | "Status":"სტატუსი", 9 | "Parameters":"პარამეტრები", 10 | "Parameter":"პარამეტრი", 11 | "Value":"მნიშვნელობა", 12 | "Description":"აღწერა", 13 | "Parameter Type":"პარამეტრის ტიპი", 14 | "Data Type":"მონაცემის ტიპი", 15 | "Response Messages":"პასუხი", 16 | "HTTP Status Code":"HTTP სტატუსი", 17 | "Reason":"მიზეზი", 18 | "Response Model":"რესპონს მოდელი", 19 | "Request URL":"მოთხოვნის URL", 20 | "Response Body":"პასუხის სხეული", 21 | "Response Code":"პასუხის კოდი", 22 | "Response Headers":"პასუხის ჰედერები", 23 | "Hide Response":"დამალე პასუხი", 24 | "Headers":"ჰედერები", 25 | "Try it out!":"ცადე !", 26 | "Show/Hide":"გამოჩენა/დამალვა", 27 | "List Operations":"ოპერაციების სია", 28 | "Expand Operations":"ოპერაციები ვრცლად", 29 | "Raw":"ნედლი", 30 | "can't parse JSON. Raw result":"JSON-ის დამუშავება ვერ მოხერხდა. ნედლი პასუხი", 31 | "Example Value":"მაგალითი", 32 | "Model Schema":"მოდელის სტრუქტურა", 33 | "Model":"მოდელი", 34 | "Click to set as parameter value":"პარამეტრისთვის მნიშვნელობის მისანიჭებლად, დააკლიკე", 35 | "apply":"გამოყენება", 36 | "Username":"მოხმარებელი", 37 | "Password":"პაროლი", 38 | "Terms of service":"მომსახურების პირობები", 39 | "Created by":"შექმნა", 40 | "See more at":"ნახე ვრცლად", 41 | "Contact the developer":"დაუკავშირდი დეველოპერს", 42 | "api version":"api ვერსია", 43 | "Response Content Type":"პასუხის კონტენტის ტიპი", 44 | "Parameter content type:":"პარამეტრის კონტენტის ტიპი:", 45 | "fetching resource":"რესურსების მიღება", 46 | "fetching resource list":"რესურსების სიის მიღება", 47 | "Explore":"ნახვა", 48 | "Show Swagger Petstore Example Apis":"ნახე Swagger Petstore სამაგალითო Api", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"სერვერთან დაკავშირება ვერ ხერხდება. შეამოწმეთ access-control-origin.", 50 | "Please specify the protocol for":"მიუთითეთ პროტოკოლი", 51 | "Can't read swagger JSON from":"swagger JSON წაკითხვა ვერ მოხერხდა", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"რესურსების ჩატვირთვა სრულდება. Swagger UI რენდერდება", 53 | "Unable to read api":"api წაკითხვა ვერ მოხერხდა", 54 | "from path":"მისამართიდან", 55 | "server returned":"სერვერმა დააბრუნა" 56 | }); 57 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/it.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Attenzione: Deprecato", 6 | "Implementation Notes":"Note di implementazione", 7 | "Response Class":"Classe della risposta", 8 | "Status":"Stato", 9 | "Parameters":"Parametri", 10 | "Parameter":"Parametro", 11 | "Value":"Valore", 12 | "Description":"Descrizione", 13 | "Parameter Type":"Tipo di parametro", 14 | "Data Type":"Tipo di dato", 15 | "Response Messages":"Messaggi della risposta", 16 | "HTTP Status Code":"Codice stato HTTP", 17 | "Reason":"Motivo", 18 | "Response Model":"Modello di risposta", 19 | "Request URL":"URL della richiesta", 20 | "Response Body":"Corpo della risposta", 21 | "Response Code":"Oggetto della risposta", 22 | "Response Headers":"Intestazioni della risposta", 23 | "Hide Response":"Nascondi risposta", 24 | "Try it out!":"Provalo!", 25 | "Show/Hide":"Mostra/Nascondi", 26 | "List Operations":"Mostra operazioni", 27 | "Expand Operations":"Espandi operazioni", 28 | "Raw":"Grezzo (raw)", 29 | "can't parse JSON. Raw result":"non è possibile parsare il JSON. Risultato grezzo (raw).", 30 | "Model Schema":"Schema del modello", 31 | "Model":"Modello", 32 | "apply":"applica", 33 | "Username":"Nome utente", 34 | "Password":"Password", 35 | "Terms of service":"Condizioni del servizio", 36 | "Created by":"Creato da", 37 | "See more at":"Informazioni aggiuntive:", 38 | "Contact the developer":"Contatta lo sviluppatore", 39 | "api version":"versione api", 40 | "Response Content Type":"Tipo di contenuto (content type) della risposta", 41 | "fetching resource":"recuperando la risorsa", 42 | "fetching resource list":"recuperando lista risorse", 43 | "Explore":"Esplora", 44 | "Show Swagger Petstore Example Apis":"Mostra le api di esempio di Swagger Petstore", 45 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Non è possibile leggere dal server. Potrebbe non avere le impostazioni di controllo accesso origine (access-control-origin) appropriate.", 46 | "Please specify the protocol for":"Si prega di specificare il protocollo per", 47 | "Can't read swagger JSON from":"Impossibile leggere JSON swagger da:", 48 | "Finished Loading Resource Information. Rendering Swagger UI":"Lettura informazioni risorse termianta. Swagger UI viene mostrata", 49 | "Unable to read api":"Impossibile leggere la api", 50 | "from path":"da cartella", 51 | "server returned":"il server ha restituito" 52 | }); 53 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/es.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Advertencia: Obsoleto", 6 | "Implementation Notes":"Notas de implementación", 7 | "Response Class":"Clase de la Respuesta", 8 | "Status":"Status", 9 | "Parameters":"Parámetros", 10 | "Parameter":"Parámetro", 11 | "Value":"Valor", 12 | "Description":"Descripción", 13 | "Parameter Type":"Tipo del Parámetro", 14 | "Data Type":"Tipo del Dato", 15 | "Response Messages":"Mensajes de la Respuesta", 16 | "HTTP Status Code":"Código de Status HTTP", 17 | "Reason":"Razón", 18 | "Response Model":"Modelo de la Respuesta", 19 | "Request URL":"URL de la Solicitud", 20 | "Response Body":"Cuerpo de la Respuesta", 21 | "Response Code":"Código de la Respuesta", 22 | "Response Headers":"Encabezados de la Respuesta", 23 | "Hide Response":"Ocultar Respuesta", 24 | "Try it out!":"Pruébalo!", 25 | "Show/Hide":"Mostrar/Ocultar", 26 | "List Operations":"Listar Operaciones", 27 | "Expand Operations":"Expandir Operaciones", 28 | "Raw":"Crudo", 29 | "can't parse JSON. Raw result":"no puede parsear el JSON. Resultado crudo", 30 | "Example Value":"Valor de Ejemplo", 31 | "Model Schema":"Esquema del Modelo", 32 | "Model":"Modelo", 33 | "apply":"aplicar", 34 | "Username":"Nombre de usuario", 35 | "Password":"Contraseña", 36 | "Terms of service":"Términos de Servicio", 37 | "Created by":"Creado por", 38 | "See more at":"Ver más en", 39 | "Contact the developer":"Contactar al desarrollador", 40 | "api version":"versión de la api", 41 | "Response Content Type":"Tipo de Contenido (Content Type) de la Respuesta", 42 | "fetching resource":"buscando recurso", 43 | "fetching resource list":"buscando lista del recurso", 44 | "Explore":"Explorar", 45 | "Show Swagger Petstore Example Apis":"Mostrar Api Ejemplo de Swagger Petstore", 46 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"No se puede leer del servidor. Tal vez no tiene la configuración de control de acceso de origen (access-control-origin) apropiado.", 47 | "Please specify the protocol for":"Por favor, especificar el protocola para", 48 | "Can't read swagger JSON from":"No se puede leer el JSON de swagger desde", 49 | "Finished Loading Resource Information. Rendering Swagger UI":"Finalizada la carga del recurso de Información. Mostrando Swagger UI", 50 | "Unable to read api":"No se puede leer la api", 51 | "from path":"desde ruta", 52 | "server returned":"el servidor retornó" 53 | }); 54 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/fr.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Avertissement : Obsolète", 6 | "Implementation Notes":"Notes d'implémentation", 7 | "Response Class":"Classe de la réponse", 8 | "Status":"Statut", 9 | "Parameters":"Paramètres", 10 | "Parameter":"Paramètre", 11 | "Value":"Valeur", 12 | "Description":"Description", 13 | "Parameter Type":"Type du paramètre", 14 | "Data Type":"Type de données", 15 | "Response Messages":"Messages de la réponse", 16 | "HTTP Status Code":"Code de statut HTTP", 17 | "Reason":"Raison", 18 | "Response Model":"Modèle de réponse", 19 | "Request URL":"URL appelée", 20 | "Response Body":"Corps de la réponse", 21 | "Response Code":"Code de la réponse", 22 | "Response Headers":"En-têtes de la réponse", 23 | "Hide Response":"Cacher la réponse", 24 | "Headers":"En-têtes", 25 | "Try it out!":"Testez !", 26 | "Show/Hide":"Afficher/Masquer", 27 | "List Operations":"Liste des opérations", 28 | "Expand Operations":"Développer les opérations", 29 | "Raw":"Brut", 30 | "can't parse JSON. Raw result":"impossible de décoder le JSON. Résultat brut", 31 | "Example Value":"Exemple la valeur", 32 | "Model Schema":"Définition du modèle", 33 | "Model":"Modèle", 34 | "apply":"appliquer", 35 | "Username":"Nom d'utilisateur", 36 | "Password":"Mot de passe", 37 | "Terms of service":"Conditions de service", 38 | "Created by":"Créé par", 39 | "See more at":"Voir plus sur", 40 | "Contact the developer":"Contacter le développeur", 41 | "api version":"version de l'api", 42 | "Response Content Type":"Content Type de la réponse", 43 | "fetching resource":"récupération de la ressource", 44 | "fetching resource list":"récupération de la liste de ressources", 45 | "Explore":"Explorer", 46 | "Show Swagger Petstore Example Apis":"Montrer les Apis de l'exemple Petstore de Swagger", 47 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Impossible de lire à partir du serveur. Il se peut que les réglages access-control-origin ne soient pas appropriés.", 48 | "Please specify the protocol for":"Veuillez spécifier un protocole pour", 49 | "Can't read swagger JSON from":"Impossible de lire le JSON swagger à partir de", 50 | "Finished Loading Resource Information. Rendering Swagger UI":"Chargement des informations terminé. Affichage de Swagger UI", 51 | "Unable to read api":"Impossible de lire l'api", 52 | "from path":"à partir du chemin", 53 | "server returned":"réponse du serveur" 54 | }); 55 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lang/el.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint quotmark: double */ 4 | window.SwaggerTranslator.learn({ 5 | "Warning: Deprecated":"Προειδοποίηση: Έχει αποσυρθεί", 6 | "Implementation Notes":"Σημειώσεις Υλοποίησης", 7 | "Response Class":"Απόκριση", 8 | "Status":"Κατάσταση", 9 | "Parameters":"Παράμετροι", 10 | "Parameter":"Παράμετρος", 11 | "Value":"Τιμή", 12 | "Description":"Περιγραφή", 13 | "Parameter Type":"Τύπος Παραμέτρου", 14 | "Data Type":"Τύπος Δεδομένων", 15 | "Response Messages":"Μηνύματα Απόκρισης", 16 | "HTTP Status Code":"Κωδικός Κατάστασης HTTP", 17 | "Reason":"Αιτιολογία", 18 | "Response Model":"Μοντέλο Απόκρισης", 19 | "Request URL":"URL Αιτήματος", 20 | "Response Body":"Σώμα Απόκρισης", 21 | "Response Code":"Κωδικός Απόκρισης", 22 | "Response Headers":"Επικεφαλίδες Απόκρισης", 23 | "Hide Response":"Απόκρυψη Απόκρισης", 24 | "Headers":"Επικεφαλίδες", 25 | "Try it out!":"Δοκιμάστε το!", 26 | "Show/Hide":"Εμφάνιση/Απόκρυψη", 27 | "List Operations":"Λίστα Λειτουργιών", 28 | "Expand Operations":"Ανάπτυξη Λειτουργιών", 29 | "Raw":"Ακατέργαστο", 30 | "can't parse JSON. Raw result":"αδυναμία ανάλυσης JSON. Ακατέργαστο αποτέλεσμα", 31 | "Example Value":"Παράδειγμα Τιμής", 32 | "Model Schema":"Σχήμα Μοντέλου", 33 | "Model":"Μοντέλο", 34 | "Click to set as parameter value":"Πατήστε για να θέσετε τιμή παραμέτρου", 35 | "apply":"εφαρμογή", 36 | "Username":"Όνομα χρήση", 37 | "Password":"Κωδικός πρόσβασης", 38 | "Terms of service":"Όροι χρήσης", 39 | "Created by":"Δημιουργήθηκε από", 40 | "See more at":"Δείτε περισσότερα στο", 41 | "Contact the developer":"Επικοινωνήστε με τον προγραμματιστή", 42 | "api version":"έκδοση api", 43 | "Response Content Type":"Τύπος Περιεχομένου Απόκρισης", 44 | "Parameter content type:":"Τύπος περιεχομένου παραμέτρου:", 45 | "fetching resource":"παραλαβή πόρου", 46 | "fetching resource list":"παραλαβή λίστας πόρων", 47 | "Explore":"Εξερεύνηση", 48 | "Show Swagger Petstore Example Apis":"Εμφάνιση Api Δειγμάτων Petstore του Swagger", 49 | "Can't read from server. It may not have the appropriate access-control-origin settings.":"Αδυναμία ανάγνωσης από τον εξυπηρετητή. Μπορεί να μην έχει κατάλληλες ρυθμίσεις για access-control-origin.", 50 | "Please specify the protocol for":"Παρακαλώ προσδιορίστε το πρωτόκολλο για", 51 | "Can't read swagger JSON from":"Αδυναμία ανάγνωσης swagger JSON από", 52 | "Finished Loading Resource Information. Rendering Swagger UI":"Ολοκλήρωση Φόρτωσης Πληροφορικών Πόρου. Παρουσίαση Swagger UI", 53 | "Unable to read api":"Αδυναμία ανάγνωσης api", 54 | "from path":"από το μονοπάτι", 55 | "server returned":"ο εξυπηρετηρής επέστρεψε" 56 | }); 57 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/css/style.css: -------------------------------------------------------------------------------- 1 | .swagger-section #header a#logo{font-size:1.5em;font-weight:700;text-decoration:none;padding:20px 0 20px 40px}#text-head{font-size:80px;font-family:Roboto,sans-serif;color:#fff;float:right;margin-right:20%}.navbar-fixed-top .navbar-brand,.navbar-fixed-top .navbar-nav,.navbar-header{height:auto}.navbar-inverse{background-color:#000;border-color:#000}#navbar-brand{margin-left:20%}.navtext{font-size:10px}.h1,h1{font-size:60px}.navbar-default .navbar-header .navbar-brand{color:#a2dfee}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a{color:#393939;font-family:Arvo,serif;font-size:1.5em}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2 a:hover{color:#000}.swagger-section .swagger-ui-wrap ul#resources li.resource div.heading h2{color:#525252;padding-left:0;display:block;clear:none;float:left;font-family:Arvo,serif;font-weight:700}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#0a0a0a}.container1{width:1500px;margin:auto;margin-top:0;background-repeat:no-repeat;background-position:-40px -20px;margin-bottom:210px}.container-inner{width:1200px;margin:auto;background-color:hsla(192,8%,88%,.75);padding-bottom:40px;padding-top:40px;border-radius:15px}.header-content{padding:0;width:1000px}.title1{font-size:80px;font-family:Vollkorn,serif;color:#404040;text-align:center;padding-top:40px;padding-bottom:100px}#icon{margin-top:-18px}.subtext{font-size:25px;font-style:italic;color:#08b;text-align:right;padding-right:250px}.bg-primary{background-color:#00468b}.navbar-default .nav>li>a,.navbar-default .nav>li>a:focus,.navbar-default .nav>li>a:focus:hover,.navbar-default .nav>li>a:hover{color:#08b}.text-faded{font-size:25px;font-family:Vollkorn,serif}.section-heading{font-family:Vollkorn,serif;font-size:45px;padding-bottom:10px}hr{border-color:#00468b;padding-bottom:10px}.description{margin-top:20px;padding-bottom:200px}.description li{font-family:Vollkorn,serif;font-size:25px;color:#525252;margin-left:28%;padding-top:5px}.gap{margin-top:200px}.troubleshootingtext{color:hsla(0,0%,100%,.7);padding-left:30%}.troubleshootingtext li{list-style-type:circle;font-size:25px;padding-bottom:5px}.overlay{position:absolute;top:0;left:0;width:100%;height:100%;z-index:1}.block.response_body.json:hover{cursor:pointer}.backdrop{color:blue}#myModal{height:100%}.modal-backdrop{bottom:0;position:fixed}.curl{padding:10px;font-family:Anonymous Pro,Menlo,Consolas,Bitstream Vera Sans Mono,Courier New,monospace;font-size:.9em;max-height:400px;margin-top:5px;overflow-y:auto;background-color:#fcf6db;border:1px solid #e5e0c6;border-radius:4px}.curl_title{font-size:1.1em;margin:0;padding:15px 0 5px;font-family:Open Sans,Helvetica Neue,Arial,sans-serif;font-weight:500;line-height:1.1}.footer{display:none}.swagger-section .swagger-ui-wrap h2{padding:0}h2{margin:0;margin-bottom:5px}.markdown p,.swagger-section .swagger-ui-wrap .code{font-size:15px;font-family:Arvo,serif}.swagger-section .swagger-ui-wrap b{font-family:Arvo,serif}#signin:hover{cursor:pointer}.dropdown-menu{padding:15px}.navbar-right .dropdown-menu{left:0;right:auto}#signinbutton{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b}.navbar-default .nav>li .details{color:#000;text-transform:none;font-size:15px;font-weight:400;font-family:Open Sans,sans-serif;font-style:italic;line-height:20px;top:-2px}.navbar-default .nav>li .details:hover{color:#000}#signout{width:100%;height:32px;font-size:13px;font-weight:700;color:#08b} -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/jquery.ba-bbq.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){function n(e){return"string"==typeof e}function r(e){var t=g.call(arguments,1);return function(){return e.apply(this,t.concat(g.call(arguments)))}}function o(e){return e.replace(/^[^#]*#?(.*)$/,"$1")}function a(e){return e.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function i(r,o,a,i,c){var u,s,p,h,d;return i!==f?(p=a.match(r?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/),d=p[3]||"",2===c&&n(i)?s=i.replace(r?R:E,""):(h=l(p[2]),i=n(i)?l[r?A:w](i):i,s=2===c?i:1===c?e.extend({},i,h):e.extend({},h,i),s=b(s),r&&(s=s.replace(m,y))),u=p[1]+(r?"#":s||!p[1]?"?":"")+s+d):u=o(a!==f?a:t[S][q]),u}function c(e,t,r){return t===f||"boolean"==typeof t?(r=t,t=b[e?A:w]()):t=n(t)?t.replace(e?R:E,""):t,l(t,r)}function u(t,r,o,a){return n(o)||"object"==typeof o||(a=o,o=r,r=f),this.each(function(){var n=e(this),i=r||v()[(this.nodeName||"").toLowerCase()]||"",c=i&&n.attr(i)||"";n.attr(i,b[t](c,o,a))})}var f,s,l,p,h,d,v,m,g=Array.prototype.slice,y=decodeURIComponent,b=e.param,$=e.bbq=e.bbq||{},x=e.event.special,j="hashchange",w="querystring",A="fragment",N="elemUrlAttr",S="location",q="href",C="src",E=/^.*\?|#.*$/g,R=/^.*\#/,U={};b[w]=r(i,0,a),b[A]=s=r(i,1,o),s.noEscape=function(t){t=t||"";var n=e.map(t.split(""),encodeURIComponent);m=new RegExp(n.join("|"),"g")},s.noEscape(",/"),e.deparam=l=function(t,n){var r={},o={"true":!0,"false":!1,"null":null};return e.each(t.replace(/\+/g," ").split("&"),function(t,a){var i,c=a.split("="),u=y(c[0]),s=r,l=0,p=u.split("]["),h=p.length-1;if(/\[/.test(p[0])&&/\]$/.test(p[h])?(p[h]=p[h].replace(/\]$/,""),p=p.shift().split("[").concat(p),h=p.length-1):h=0,2===c.length)if(i=y(c[1]),n&&(i=i&&!isNaN(i)?+i:"undefined"===i?f:o[i]!==f?o[i]:i),h)for(;l<=h;l++)u=""===p[l]?s.length:p[l],s=s[u]=l').hide().insertAfter("body")[0].contentWindow,s=function(){return r(a.document[i][u])},(f=function(e,t){if(e!==t){var n=a.document;n.open().close(),n[i].hash="#"+e}})(r()))}var o,a,f,s,p={};return p.start=function(){if(!o){var a=r();f||n(),function l(){var n=r(),p=s(a);n!==a?(f(a=n,p),e(t).trigger(c)):p!==a&&(t[i][u]=t[i][u].replace(/#.*/,"")+"#"+p),o=setTimeout(l,e[c+"Delay"])}()}},p.stop=function(){a||(o&&clearTimeout(o),o=0)},p}()}(jQuery,this); -------------------------------------------------------------------------------- /test/2_forex_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const assert = require('assert'); 16 | const {codes} = require('../lib/errors'); 17 | const testDB = require('./testDB'); 18 | 19 | let Book; 20 | describe('ALE forex journals', () => { 21 | 22 | before(() => { 23 | return testDB.clear(); 24 | }); 25 | 26 | before(() => { 27 | const sequelize = require('../models/connection'); 28 | Book = require('../models/book'); 29 | return sequelize.sync(); 30 | }); 31 | 32 | it('specify currencies in journal entries', () => { 33 | let book; 34 | return Book.getOrCreateBook('Forex test', 'USD').then(b => { 35 | book = b.book; 36 | const entry = book.newJournalEntry('Base investment'); 37 | return entry 38 | .credit('Trading:USD', 1650, 'USD', 1) 39 | .debit('Assets:Bank', 1650, 'USD', 1) 40 | .commit(); 41 | }).then(() => { 42 | return book.getBalance({account: ['Assets', 'Trading']}); 43 | }).then(result => { 44 | assert.equal(result.balance, 0); 45 | assert.equal(result.numTransactions, 2); 46 | }); 47 | }); 48 | 49 | it('handles multi-currency entries', () => { 50 | let book; 51 | return Book.getOrCreateBook('Forex test', 'USD').then(b => { 52 | book = b.book; 53 | const entry = book.newJournalEntry('Buy 10000 ZAR for $1000'); 54 | return entry 55 | .credit('Trading:ZAR', 10000, 'ZAR', 0.1) 56 | .debit('Trading:USD', 1050, 'USD', 1) 57 | .credit('Expenses:Fees', 50, 'USD', 1) 58 | .commit(); 59 | }).then(() => { 60 | return book.getBalance({account: ['Trading']}, true); 61 | }).then(result => { 62 | assert.equal(result.balance, 1600); 63 | assert.equal(result.numTransactions, 3); 64 | assert.equal(result.currency, 'USD'); 65 | }); 66 | }); 67 | 68 | it('normalizes exchange rates', () => { 69 | const norm = Book.normalizeRates('USD', {ZAR: 1, USD: 0.1, EUR: 0.08}); 70 | assert.deepEqual(norm, {ZAR: 10, USD: 1, EUR: 0.8}); 71 | }); 72 | 73 | it('Calculates mark-to-market at a given set of rates', () => { 74 | return Book.getOrCreateBook('Forex test').then(res => { 75 | return res.book.markToMarket({account: ['Trading', 'Assets:Bank']}, {ZAR: 20, USD: 1}); 76 | }).then(result => { 77 | assert.equal(result['Trading:ZAR'], 500); 78 | assert.equal(result['Trading:USD'], 600); 79 | assert.equal(result.unrealizedProfit, -550); 80 | }); 81 | }); 82 | 83 | it('Rejects mark-to-market if rates are missing', () => { 84 | return Book.getOrCreateBook('Forex test').then(res => { 85 | return res.book.markToMarket({account: ['Trading', 'Assets:Bank']}, {USD: 1}); 86 | }).then(() => { 87 | throw new Error('Should throw'); 88 | }, err => { 89 | assert(err); 90 | assert.equal(err.code, codes.ExchangeRateNotFound); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 91 | 92 | 93 | 94 | 104 | 105 |
 
106 |
107 | 108 | 109 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/swagger-oauth.js: -------------------------------------------------------------------------------- 1 | function handleLogin(){var e=[],o=window.swaggerUiAuth.authSchemes||window.swaggerUiAuth.securityDefinitions;if(o){var i,n=o;for(i in n){var a=n[i];if("oauth2"===a.type&&a.scopes){var t;if(Array.isArray(a.scopes)){var p;for(p=0;p','
Select OAuth2.0 Scopes
','
',"

Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.",'Learn how to use',"

","

"+appName+" API requires the following scopes. Select which ones you want to grant to Swagger UI.

",'
    ',"
",'

','
',"
",""].join("")),$(document.body).append(popupDialog),popup=popupDialog.find("ul.api-popup-scopes").empty(),p=0;p",popup.append(str);var r=$(window),s=r.width(),c=r.height(),l=r.scrollTop(),d=popupDialog.outerWidth(),u=popupDialog.outerHeight(),h=(c-u)/2+l,g=(s-d)/2;popupDialog.css({top:(h<0?0:h)+"px",left:(g<0?0:g)+"px"}),popupDialog.find("button.api-popup-cancel").click(function(){popupMask.hide(),popupDialog.hide(),popupDialog.empty(),popupDialog=[]}),$("button.api-popup-authbtn").unbind(),popupDialog.find("button.api-popup-authbtn").click(function(){function e(e){return e.vendorExtensions["x-tokenName"]||e.tokenName}popupMask.hide(),popupDialog.hide();var o,i=window.swaggerUi.api.authSchemes,n=window.location,a=location.pathname.substring(0,location.pathname.lastIndexOf("/")),t=n.protocol+"//"+n.host+a+"/o2c.html",p=window.oAuthRedirectUrl||t,r=null,s=[],c=popup.find("input:checked"),l=[];for(k=0;k0?void log("auth unable initialize oauth: "+i):($("pre code").each(function(e,o){hljs.highlightBlock(o)}),$(".api-ic").unbind(),void $(".api-ic").click(function(e){$(e.target).hasClass("ic-off")?handleLogin():handleLogout()}))}function clientCredentialsFlow(e,o,i){var n={client_id:clientId,client_secret:clientSecret,scope:e.join(" "),grant_type:"client_credentials"};$.ajax({url:o,type:"POST",data:n,success:function(e,o,n){onOAuthComplete(e,i)},error:function(e,o,i){onOAuthComplete("")}})}var appName,popupMask,popupDialog,clientId,realm,redirect_uri,clientSecret,scopeSeparator,additionalQueryStringParams;window.processOAuthCode=function(e){var o=e.state,i=window.location,n=location.pathname.substring(0,location.pathname.lastIndexOf("/")),a=i.protocol+"//"+i.host+n+"/o2c.html",t=window.oAuthRedirectUrl||a,p={client_id:clientId,code:e.code,grant_type:"authorization_code",redirect_uri:t};clientSecret&&(p.client_secret=clientSecret),$.ajax({url:window.swaggerUiAuth.tokenUrl,type:"POST",data:p,success:function(e,i,n){onOAuthComplete(e,o)},error:function(e,o,i){onOAuthComplete("")}})},window.onOAuthComplete=function(e,o){if(e)if(e.error){var i=$("input[type=checkbox],.secured");i.each(function(e){i[e].checked=!1}),alert(e.error)}else{var n=e[window.swaggerUiAuth.tokenName];if(o||(o=e.state),n){var a=null;$.each($(".auth .api-ic .api_information_panel"),function(e,o){var i=o;if(i&&i.childNodes){var n=[];$.each(i.childNodes,function(e,o){var i=o.innerHTML;i&&n.push(i)});for(var t=[],p=0;p0?(a=o.parentNode.parentNode,$(a.parentNode).find(".api-ic.ic-on").addClass("ic-off"),$(a.parentNode).find(".api-ic.ic-on").removeClass("ic-on"),$(a).find(".api-ic").addClass("ic-warning"),$(a).find(".api-ic").removeClass("ic-error")):(a=o.parentNode.parentNode,$(a.parentNode).find(".api-ic.ic-off").addClass("ic-on"),$(a.parentNode).find(".api-ic.ic-off").removeClass("ic-off"),$(a).find(".api-ic").addClass("ic-info"),$(a).find(".api-ic").removeClass("ic-warning"),$(a).find(".api-ic").removeClass("ic-error"))}}),"undefined"!=typeof window.swaggerUi&&(window.swaggerUi.api.clientAuthorizations.add(window.swaggerUiAuth.OAuthSchemeKey,new SwaggerClient.ApiKeyAuthorization("Authorization","Bearer "+n,"header")),window.swaggerUi.load())}}}; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo is no longer maintained. 2 | 3 | # A=L+E 4 | Multi-currency double-entry accounting system based on node.js + Sequelize 5 | 6 | Ale is based on the basic formula `Assets = Liabilities + Equity` and is insipred by [Medici](https://github.com/koresar/medici) 7 | 8 | ## Standalone Server 9 | A.L.E. operates as a standalone microservice. 10 | 11 | Configure your database and after setting the `ALE_CONNECTION` envar, simply run 12 | 13 | `$ node server.js` 14 | 15 | to launch a REST server to handle all your double-entry accounting needs. 16 | You can peruse the [API documentation](https://cjs77.github.io/ale/) for more details on how to use the service. 17 | 18 | 19 | 20 | ## Programmatic API 21 | A.L.E. can also be used as a node module. This section describes how to manage multi-currency accounting programmatically. 22 | 23 | ALE divides itself into "books", each of which store *journal entries* and their child *transactions*. The cardinal rule of double-entry accounting is that "everything must balance out to zero", and that rule is applied to every journal entry written to the book. If the transactions for a journal entry do not balance out to zero, the system will return a rejected promise. 24 | 25 | Books simply represent the physical book in which you would record your transactions - on a technical level, the "book" attribute simply is added as a field in `Transactions` and `JournalEntry` tables to allow you to have multiple books if you want to. 26 | 27 | Each transaction in ALE operates on a single *account*. Accounts are arbitrary string tags, but you can subdivide accounts using colons (or any other separator). Transactions in the `Assets:Cash` account will appear in a query for transactions in the `Assets` account, but will not appear in a query for transactions in the `Assets:Property` account. This allows you to query, for example, all expenses, or just "office overhead" expenses (Expenses:Office Overhead). 28 | 29 | In theory, the account names are entirely arbitrary, but you will likely want to use traditional accounting sections and subsections like assets, expenses, income, accounts receivable, accounts payable, etc. But, in the end, how you structure the accounts is entirely up to you. Sub-accounts are also not matched explicitly, but by comparing the account name to query against the beginning of the account name. Thus `Trades` will match `Trades:USD`, but not `Assets:Trades`. 30 | 31 | 32 | ## Configuration 33 | 34 | ALE tries to be agnostic as to which RDMS you have running on the back-end. Therefore, you need to ensure that the relevant DB bindings are installed and part of your `package.json` file. 35 | 36 | For PostgreSQL, this would entail 37 | 38 | `yarn add pg` 39 | 40 | You *must* set an `ALE_CONNECTION` environment variable which holds the connection string to connect to the underlying database. To date, 41 | ALE has been tested against PostgreSQL, but any DB supported by Sequelize (SQLite, MySQL etc) should work. 42 | 43 | `export ALE_CONNECTION=postres://ale_user@localhost:5432/trading_db` 44 | 45 | The user and database must exist before importing ALE. For an example of how to ensure this in code (for PostgreSQL), see the test setup function. 46 | 47 | 48 | ## Coding Standards 49 | 50 | ALE is written in JavaScript. All database queries return promise objects instead of using the traditional node `function(err, result)`callback. 51 | 52 | ## Writing journal entries 53 | 54 | Writing a journal entry is very simple. First you need a `book` object: 55 | 56 | ```js 57 | const { Book } = require('a.l.e'); 58 | 59 | // The first argument is the book name, which is used to determine which book the transactions and journals are queried from. 60 | // The second argument is the currency to report all queries in. 61 | return Book.getOrCreateBook('ACME inc', 'USD').then(book => { 62 | ... 63 | }); 64 | ``` 65 | 66 | Now write an entry: 67 | 68 | ```js 69 | // You can specify a Date object as the second argument in the book.entry() method if you want the transaction to be for a different date than right now 70 | const newEntry= book.newJournalEntry('Received payment'); 71 | newEntry.debit('Assets:Receivable', 500) // The currency and exchange rate default to 'USD' and 1.0 if omitted 72 | .credit('Income:Rent', 500, 'USD'); // debit and credit return the entry to allow chained transactions 73 | entry.commit().then((e) => { // Validate and commit the entry to the DB, returning a promise 74 | assert.equal(e.memo, 'Received payment'); 75 | }); 76 | ``` 77 | 78 | You can continue to chain debits and credits to the journal object until you are finished. The `entry.debit()` and `entry.credit()` methods both have the same arguments: (account, amount, currency, exchangeRate). 79 | 80 | ## Querying Account Balance 81 | 82 | To query account balance, just use the `book.balance()` method: 83 | 84 | ```js 85 | myBook.balance({ account:'Assets:Accounts Receivable' }).then((balance) => { 86 | console.log("Joe Blow owes me", balance.total); 87 | }); 88 | ``` 89 | 90 | `balance` returns a promise that resolves to the the following `creditTotal` - the sum of credits (in quote currency), `debitTotal`, `balance` - The current balance, in quote currency, `currency` - the currency the results are reported in, `numTransactions` - The total number of transactions making up the balance calculation. 91 | 92 | ## Retrieving Transactions 93 | 94 | To retrieve transactions, use the `book.ledger()` method (here I'm using moment.js for dates): 95 | 96 | ```js 97 | const startDate = moment().subtract('months', 1).toDate(); // One month ago 98 | const endDate = new Date(); //today 99 | 100 | myBook.ledger({ 101 | account: ['Income', 'Expenses'] // Both sets of accounts will be included 102 | start_date: startDate, 103 | end_date: endDate 104 | }).then((result) => { 105 | // Do something with the returned transaction documents 106 | result.count // The number of transactions found 107 | result.transactions // An array of transactions found 108 | }); 109 | ``` 110 | 111 | ## Voiding Journal Entries 112 | 113 | Sometimes you will make an entry that turns out to be inaccurate or that otherwise needs to be voided. Keeping with traditional double-entry accounting, instead of simply deleting that journal entry, ALE instead will mark the entry as "voided", and then add an equal, opposite journal entry to offset the transactions in the original. This gives you a clear picture of all actions taken with your book. 114 | 115 | To void a journal entry, you can either call the `voidEntry(book, void_reason)` method on a JournalEntry instance document, or use the `book.voidEntry(journalId, void_reason)` method if you know the journal document's ID. 116 | 117 | ```js 118 | myBook.void("123456", "I made a mistake").then(() => { 119 | // Do something after voiding 120 | }) 121 | ``` 122 | 123 | The original entry will have its `voided` field set to true and counteracting transctions will be added with the original memo suffixed with `[REVERSED]` 124 | 125 | 126 | ## Calculating unrealized profit with `markToMarket` 127 | 128 | You can calculated unrealised profit/losses due to changes in exhange rates by calling `markToMarket` and giving it a hash of exchange rates: 129 | 130 | ``` 131 | const book = new Book('Forex test'); 132 | book.markToMarket({ account: ['Trading', 'Assets:Bank'] }, { ZAR: 20, USD: 1 }).then(result => { 133 | assert.equal(result['Trading:ZAR'], 500); 134 | assert.equal(result['Trading:USD'], 600); 135 | assert.equal(result['Assets:Bank'], -1650); 136 | assert.equal(result.unrealizedProfit, -550); 137 | }); 138 | ``` 139 | 140 | ## Changelog 141 | 142 | * **v0.1.0** First release 143 | * **v1.0.0** REST API release 144 | -------------------------------------------------------------------------------- /models/journal.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | const Sequelize = require('sequelize'); 15 | const sequelize = require('./connection'); 16 | const Transaction = require('./transaction'); 17 | const {NEAR_ZERO} = require('./types'); 18 | const {AleError, codes} = require('../lib/errors'); 19 | 20 | /** 21 | * A journal entry comprises a set of transactions, which have a balance set of debit and credit entries 22 | */ 23 | const JournalEntry = sequelize.define('journal', { 24 | memo: {type: Sequelize.TEXT, defaultValue: ''}, 25 | timestamp: {type: Sequelize.DATE, validate: {isDate: true}, defaultValue: Date.now}, 26 | voided: {type: Sequelize.BOOLEAN, defaultValue: false}, 27 | voidReason: Sequelize.STRING 28 | }, { 29 | name: { 30 | singular: 'JournalEntry', 31 | plural: 'JournalEntries' 32 | } 33 | } 34 | ); 35 | 36 | Transaction.JournalEntry = Transaction.belongsTo(JournalEntry, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}); 37 | JournalEntry.hasOne(JournalEntry, {as: 'Original'}); 38 | JournalEntry.hasMany(Transaction, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}); 39 | 40 | JournalEntry.prototype.values = function() { 41 | return { 42 | id: this.getDataValue('id'), 43 | memo: this.getDataValue('memo'), 44 | timestamp: new Date(this.getDataValue('timestamp')), 45 | voided: this.getDataValue('voided'), 46 | voidReason: this.getDataValue('voidReason'), 47 | OriginalId: this.getDataValue('OriginalId') 48 | }; 49 | }; 50 | 51 | JournalEntry.prototype.voidEntry = function(book, reason) { 52 | if (this.voided === true) { 53 | return sequelize.Promise.reject(new Error('Journal entry already voided')); 54 | } 55 | return sequelize.transaction({autocommit: false}, t => { 56 | // Set this to void with reason and also set all associated transactions 57 | this.voided = true; 58 | this.voidReason = !reason ? '' : reason; 59 | return this.save({transaction: t}).then(oldEntry => { 60 | return oldEntry.getTransactions(); 61 | }).then(txs => { 62 | return sequelize.Promise.map(txs, tx => { 63 | tx.voided = true; 64 | tx.voidReason = reason; 65 | return tx.save({transaction: t}); 66 | }); 67 | }).then(txs => { 68 | let newMemo = `${this.memo} [REVERSED]`; 69 | // Ok now create an equal and opposite journal 70 | const newEntry = book.newJournalEntry(newMemo, new Date()); 71 | newEntry.setDataValue('OriginalId', this.getDataValue('id')); 72 | for (let tx of txs) { 73 | if (+tx.credit !== 0) { 74 | newEntry.debit(tx.account, tx.credit, tx.currency, +tx.exchangeRate); 75 | } 76 | if (+tx.debit !== 0) { 77 | newEntry.credit(tx.account, tx.debit, tx.currency, tx.exchangeRate); 78 | } 79 | } 80 | const total = this.calculatePendingTotal(txs); 81 | if (Math.abs(total) > NEAR_ZERO) { 82 | return sequelize.Promise.reject(new AleError('Invalid Journal Entry Reversal. Total not zero', codes.EntryNotBalanced)); 83 | } 84 | return newEntry._saveEntryWithTxs(t); 85 | }); 86 | }).then(() => { 87 | //transaction has been committed 88 | return true; 89 | }).catch(e => { 90 | const err = new AleError(`Voiding entry failed. ${e.message}`, codes.DatabaseUpdateError); 91 | return sequelize.Promise.reject(err); 92 | }); 93 | }; 94 | 95 | JournalEntry.prototype.newTransaction = function(account, amount, isCredit, currency, exchangeRate = 1.0) { 96 | amount = +amount; 97 | if (typeof account === 'string') { 98 | account = account.split(':'); 99 | } 100 | 101 | if (account.length > 3) { 102 | const err = new Error('Account path is too deep (maximum 3)'); 103 | err.accountPath = account; 104 | throw err; 105 | } 106 | 107 | const transaction = Transaction.build({ 108 | account: account.join(':'), 109 | credit: isCredit ? amount : 0.0, 110 | debit: isCredit ? 0.0 : amount, 111 | exchangeRate: +exchangeRate, 112 | currency: currency, 113 | timestamp: new Date(), 114 | bookId: this.getDataValue('bookId') 115 | }); 116 | if (!this.pendingTransactions) { 117 | this.pendingTransactions = []; 118 | } 119 | this.pendingTransactions.push(transaction); 120 | 121 | return this; 122 | }; 123 | 124 | JournalEntry.prototype.debit = function(account, amount, currency = undefined, exchangeRate = 1.0) { 125 | return this.newTransaction(account, amount, false, currency, exchangeRate); 126 | }; 127 | 128 | JournalEntry.prototype.credit = function(account, amount, currency = undefined, exchangeRate = 1.) { 129 | return this.newTransaction(account, amount, true, currency, exchangeRate); 130 | }; 131 | 132 | JournalEntry.prototype.commit = function() { 133 | const total = this.calculatePendingTotal(); 134 | if (Math.abs(total) > NEAR_ZERO) { 135 | return sequelize.Promise.reject(new AleError('Invalid Journal Entry. Total not zero', codes.EntryNotBalanced)); 136 | } 137 | return sequelize.transaction({autocommit: false}, t => { 138 | return this._saveEntryWithTxs(t); 139 | }).then(result => { 140 | // Transaction has committed 141 | return result; 142 | }).catch(e => { 143 | // Transaction has rolled back 144 | const err = new AleError(e.message, codes.DatabaseUpdateError); 145 | return sequelize.Promise.reject(err); 146 | }); 147 | }; 148 | 149 | JournalEntry.prototype._saveEntryWithTxs = function(t) { 150 | let result; 151 | return this.save({transaction: t}).then(e => { 152 | result = e; 153 | const id = result.getDataValue('id'); 154 | return this.saveTransactions(id, {transaction: t}); 155 | }).then(() => { 156 | return sequelize.Promise.resolve(result); 157 | }); 158 | }; 159 | 160 | JournalEntry.prototype.calculatePendingTotal = function(txs) { 161 | const transactions = txs || this.pendingTransactions; 162 | if (!transactions || !Array.isArray(transactions) || transactions.length === 0) { 163 | return; 164 | } 165 | let total = 0.0; 166 | for (let tx of transactions) { 167 | total = total + tx.getDataValue('credit') * tx.getDataValue('exchangeRate'); 168 | total = total - tx.getDataValue('debit') * tx.getDataValue('exchangeRate'); 169 | } 170 | return total; 171 | }; 172 | 173 | JournalEntry.prototype.saveTransactions = function(id, opts) { 174 | const transactions = this.pendingTransactions; 175 | return sequelize.Promise.map(transactions, tx => { 176 | tx.setDataValue('JournalEntryId', id); 177 | return tx.save(opts); 178 | }).catch(e => { 179 | return sequelize.Promise.reject(new AleError(`Failed to save transactions for Entry: ${this.getDataValue('memo')} ${e.message}`, codes.DatabaseUpdateError)); 180 | }); 181 | }; 182 | 183 | module.exports = JournalEntry; 184 | -------------------------------------------------------------------------------- /test/1_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2017 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | */ 14 | 15 | const assert = require('assert'); 16 | const {codes} = require('../lib/errors'); 17 | const testDB = require('./testDB'); 18 | 19 | let Book; 20 | describe('ALE', () => { 21 | let bookZAR, bookUSD, sequelize; 22 | 23 | before(done => { 24 | testDB.clear().then(() => { 25 | done(); 26 | }); 27 | }); 28 | 29 | before(() => { 30 | sequelize = require('../models/connection'); 31 | Book = require('../models/book'); 32 | return sequelize.sync(); 33 | }); 34 | 35 | let entry1 = null; 36 | it('Should let you define new books', () => { 37 | return Book.getOrCreateBook('TestB', 'USD').then(res => { 38 | bookUSD = res.book; 39 | assert.equal(bookUSD.name, 'TestB'); 40 | assert.equal(bookUSD.quoteCurrency, 'USD'); 41 | return Book.getOrCreateBook('TestA', 'ZAR'); 42 | }).then(res => { 43 | bookZAR = res.book; 44 | assert.equal(bookZAR.name, 'TestA'); 45 | assert.equal(bookZAR.quoteCurrency, 'ZAR'); 46 | return Book.listBooks(); 47 | }).then(books => { 48 | assert.equal(books.length, 2); 49 | assert.equal(books[0].name, 'TestA'); 50 | assert.equal(books[1].name, 'TestB'); 51 | }); 52 | }); 53 | 54 | it('Requesting a book that exists returns that book', () => { 55 | return Book.getOrCreateBook('TestA', 'ZAR').then(res => { 56 | assert.equal(res.isNew, false); 57 | assert.equal(res.book.name, 'TestA'); 58 | assert.equal(res.book.quoteCurrency, 'ZAR'); 59 | return Book.listBooks(); 60 | }).then(books => { 61 | assert.equal(books.length, 2); 62 | assert.equal(books[0].name, 'TestA'); 63 | assert.equal(books[1].name, 'TestB'); 64 | }); 65 | }); 66 | 67 | it('Requesting a book with conflicting base currencies throws an error', () => { 68 | return Book.getOrCreateBook('TestA', 'THB').then(() => { 69 | throw new Error('Request should fail'); 70 | }, err => { 71 | assert(err); 72 | assert.equal(err.code, codes.MismatchedCurrency); 73 | }); 74 | }); 75 | 76 | it('should return a list of books', () => { 77 | return Book.listBooks().then(books => { 78 | assert.equal(books.length, 2); 79 | assert.equal(books[0].name, 'TestA'); 80 | assert.equal(books[0].quoteCurrency, 'ZAR'); 81 | assert.equal(books[1].name, 'TestB'); 82 | assert.equal(books[1].quoteCurrency, 'USD'); 83 | }); 84 | }); 85 | 86 | it('Should let you create a basic transaction', () => { 87 | const entry = bookZAR.newJournalEntry('Test Entry'); 88 | entry.debit('Assets:Receivable', 500, 'ZAR') 89 | .credit('Income:Rent', 500, 'ZAR'); 90 | return entry.commit().then((e) => { 91 | entry1 = e; 92 | assert.equal(e.memo, 'Test Entry'); 93 | return e.getTransactions(); 94 | }).then((txs) => { 95 | assert.equal(txs.length, 2); 96 | const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); 97 | const entry2 = bookZAR.newJournalEntry('Test Entry 2', threeDaysAgo); 98 | return entry2 99 | .debit('Assets:Receivable', 700) 100 | .credit('Income:Rent', 700) 101 | .commit(); 102 | }).then(entry2 => { 103 | assert.equal(entry2.memo, 'Test Entry 2'); 104 | return entry2.getTransactions(); 105 | }).then(txs => { 106 | txs = txs.sort((a, b) => a.account > b.account ? 1 : -1); 107 | assert.equal(txs.length, 2); 108 | assert.equal(txs[0].exchangeRate, 1.0); 109 | assert.equal(txs[0].debit, 700); 110 | assert.equal(txs[0].credit, 0); 111 | assert.equal(txs[1].credit, 700); 112 | assert.equal(txs[1].debit, 0); 113 | }); 114 | }); 115 | 116 | it('Should deal with rounding errors', () => { 117 | const entry = bookUSD.newJournalEntry('Rounding Test'); 118 | entry.credit('A:B', 1005, 'USD') 119 | .debit('A:B', 994.95, 'USD') 120 | .debit('A:B', 10.05, 'USD'); 121 | return entry.commit().then(() => { 122 | return bookUSD.getBalance({account: 'A:B'}); 123 | }).then((balance) => { 124 | assert.equal(balance.balance, 0); 125 | assert.equal(balance.creditTotal, 1005); 126 | assert.equal(balance.debitTotal, 1005); 127 | assert.equal(balance.numTransactions, 3); 128 | assert.equal(balance.currency, 'USD'); 129 | }); 130 | }); 131 | 132 | it('Should have updated the balance for assets and income and accurately give balance for sub-accounts', () => { 133 | return bookZAR.getBalance({account: 'Assets'}).then((data) => { 134 | assert.equal(data.numTransactions, 2); 135 | assert.equal(data.balance, -1200); 136 | return bookZAR.getBalance({account: 'Assets:Receivable'}); 137 | }).then((data) => { 138 | assert.equal(data.numTransactions, 2); 139 | assert.equal(data.balance, -1200); 140 | return bookZAR.getBalance({account: 'Assets:Other'}); 141 | }).then((data) => { 142 | assert.equal(data.numTransactions, 0); 143 | assert.equal(data.balance, 0); 144 | }); 145 | }); 146 | 147 | it('should return full ledger', () => { 148 | return bookZAR.getLedger().then(res => { 149 | assert.equal(res.length, 2); 150 | }); 151 | }); 152 | 153 | it('should allow you to void a journal entry', () => { 154 | return bookZAR.getJournalEntries({memo: 'Test Entry'}).then((txs) => { 155 | const id = txs[0].id; 156 | return bookZAR.voidEntry(id, 'Messed up'); 157 | }).then(() => { 158 | return bookZAR.getBalance({account: 'Assets'}); 159 | }).then((data) => { 160 | assert.equal(data.balance, -700); 161 | }); 162 | }); 163 | 164 | it('should list all accounts', () => { 165 | return bookZAR.listAccounts().then((accounts) => { 166 | assert(accounts.indexOf('Assets') > -1); 167 | assert(accounts.indexOf('Assets:Receivable') > -1); 168 | assert(accounts.indexOf('Income') > -1); 169 | assert(accounts.indexOf('Income:Rent') > -1); 170 | }); 171 | }); 172 | 173 | it('should return ledger with array of accounts', () => { 174 | return bookZAR.getTransactions({ 175 | account: ['Assets', 'Income'] 176 | }).then((result) => { 177 | assert.equal(result.length, 6); 178 | }); 179 | }); 180 | 181 | it('should give you a paginated ledger when requested', () => { 182 | return bookZAR.getTransactions({ 183 | account: ['Assets', 'Income'], 184 | perPage: 4, 185 | page: 2 186 | }) 187 | .then((result) => { 188 | assert.equal(result.length, 2); 189 | assert.equal(result[0].credit, 500); 190 | assert.equal(result[1].debit, 500); 191 | }); 192 | }); 193 | 194 | it('should return newest transactions first when requested', () => { 195 | return bookZAR.getJournalEntries() 196 | .then((res) => { 197 | assert(new Date(res[0].timestamp) < new Date(res[1].timestamp)); 198 | }) 199 | .then(() => bookZAR.getJournalEntries({newestFirst: true})) 200 | .then((res) => { 201 | assert(new Date(res[0].timestamp) > new Date(res[1].timestamp)); 202 | }); 203 | }) 204 | }); 205 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/highlight.9.1.0.pack.js: -------------------------------------------------------------------------------- 1 | !function(e){"undefined"!=typeof exports?e(exports):(self.hljs=e({}),"function"==typeof define&&define.amd&&define("hljs",[],function(){return self.hljs}))}(function(e){function r(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function n(e,r){var t=e&&e.exec(r);return t&&0==t.index}function a(e){return/^(no-?highlight|plain|text)$/i.test(e)}function c(e){var r,t,n,c=e.className+" ";if(c+=e.parentNode?e.parentNode.className:"",t=/\blang(?:uage)?-([\w-]+)\b/i.exec(c))return E(t[1])?t[1]:"no-highlight";for(c=c.split(/\s+/),r=0,n=c.length;n>r;r++)if(E(c[r])||a(c[r]))return c[r]}function i(e,r){var t,n={};for(t in e)n[t]=e[t];if(r)for(t in r)n[t]=r[t];return n}function o(e){var r=[];return function n(e,a){for(var c=e.firstChild;c;c=c.nextSibling)3==c.nodeType?a+=c.nodeValue.length:1==c.nodeType&&(r.push({event:"start",offset:a,node:c}),a=n(c,a),t(c).match(/br|hr|img|input/)||r.push({event:"stop",offset:a,node:c}));return a}(e,0),r}function s(e,n,a){function c(){return e.length&&n.length?e[0].offset!=n[0].offset?e[0].offset"}function o(e){l+=""}function s(e){("start"==e.event?i:o)(e.node)}for(var u=0,l="",f=[];e.length||n.length;){var b=c();if(l+=r(a.substr(u,b[0].offset-u)),u=b[0].offset,b==e){f.reverse().forEach(o);do s(b.splice(0,1)[0]),b=c();while(b==e&&b.length&&b[0].offset==u);f.reverse().forEach(i)}else"start"==b[0].event?f.push(b[0].node):f.pop(),s(b.splice(0,1)[0])}return l+r(a.substr(u))}function u(e){function r(e){return e&&e.source||e}function t(t,n){return new RegExp(r(t),"m"+(e.cI?"i":"")+(n?"g":""))}function n(a,c){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},s=function(r,t){e.cI&&(t=t.toLowerCase()),t.split(" ").forEach(function(e){var t=e.split("|");o[t[0]]=[r,t[1]?Number(t[1]):1]})};"string"==typeof a.k?s("keyword",a.k):Object.keys(a.k).forEach(function(e){s(e,a.k[e])}),a.k=o}a.lR=t(a.l||/\b\w+\b/,!0),c&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=t(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=t(a.e)),a.tE=r(a.e)||"",a.eW&&c.tE&&(a.tE+=(a.e?"|":"")+c.tE)),a.i&&(a.iR=t(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var u=[];a.c.forEach(function(e){e.v?e.v.forEach(function(r){u.push(i(e,r))}):u.push("self"==e?a:e)}),a.c=u,a.c.forEach(function(e){n(e,a)}),a.starts&&n(a.starts,c);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(r).filter(Boolean);a.t=l.length?t(l.join("|"),!0):{exec:function(){return null}}}}n(e)}function l(e,t,a,c){function i(e,r){for(var t=0;t";return c+=e+'">',c+r+i}function p(){if(!M.k)return r(B);var e="",t=0;M.lR.lastIndex=0;for(var n=M.lR.exec(B);n;){e+=r(B.substr(t,n.index-t));var a=b(M,n);a?(L+=a[1],e+=g(a[0],r(n[0]))):e+=r(n[0]),t=M.lR.lastIndex,n=M.lR.exec(B)}return e+r(B.substr(t))}function h(){var e="string"==typeof M.sL;if(e&&!y[M.sL])return r(B);var t=e?l(M.sL,B,!0,R[M.sL]):f(B,M.sL.length?M.sL:void 0);return M.r>0&&(L+=t.r),e&&(R[M.sL]=t.top),g(t.language,t.value,!1,!0)}function d(){return void 0!==M.sL?h():p()}function m(e,t){var n=e.cN?g(e.cN,"",!0):"";e.rB?(x+=n,B=""):e.eB?(x+=r(t)+n,B=""):(x+=n,B=t),M=Object.create(e,{parent:{value:M}})}function v(e,t){if(B+=e,void 0===t)return x+=d(),0;var n=i(t,M);if(n)return x+=d(),m(n,t),n.rB?0:t.length;var a=o(M,t);if(a){var c=M;c.rE||c.eE||(B+=t),x+=d();do M.cN&&(x+=""),L+=M.r,M=M.parent;while(M!=a.parent);return c.eE&&(x+=r(t)),B="",a.starts&&m(a.starts,""),c.rE?0:t.length}if(s(t,M))throw new Error('Illegal lexeme "'+t+'" for mode "'+(M.cN||"")+'"');return B+=t,t.length||1}var N=E(e);if(!N)throw new Error('Unknown language: "'+e+'"');u(N);var C,M=c||N,R={},x="";for(C=M;C!=N;C=C.parent)C.cN&&(x=g(C.cN,"",!0)+x);var B="",L=0;try{for(var S,A,k=0;M.t.lastIndex=k,S=M.t.exec(t),S;)A=v(t.substr(k,S.index-k),S[0]),k=S.index+A;for(v(t.substr(k)),C=M;C.parent;C=C.parent)C.cN&&(x+="");return{r:L,value:x,language:e,top:M}}catch(I){if(-1!=I.message.indexOf("Illegal"))return{r:0,value:r(t)};throw I}}function f(e,t){t=t||w.languages||Object.keys(y);var n={r:0,value:r(e)},a=n;return t.forEach(function(r){if(E(r)){var t=l(r,e,!1);t.language=r,t.r>a.r&&(a=t),t.r>n.r&&(a=n,n=t)}}),a.language&&(n.second_best=a),n}function b(e){return w.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,r){return r.replace(/\t/g,w.tabReplace)})),w.useBR&&(e=e.replace(/\n/g,"
")),e}function g(e,r,t){var n=r?C[r]:t,a=[e.trim()];return e.match(/\bhljs\b/)||a.push("hljs"),-1===e.indexOf(n)&&a.push(n),a.join(" ").trim()}function p(e){var r=c(e);if(!a(r)){var t;w.useBR?(t=document.createElementNS("http://www.w3.org/1999/xhtml","div"),t.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):t=e;var n=t.textContent,i=r?l(r,n,!0):f(n),u=o(t);if(u.length){var p=document.createElementNS("http://www.w3.org/1999/xhtml","div");p.innerHTML=i.value,i.value=s(u,o(p),n)}i.value=b(i.value),e.innerHTML=i.value,e.className=g(e.className,r,i.language),e.result={language:i.language,re:i.r},i.second_best&&(e.second_best={language:i.second_best.language,re:i.second_best.r})}}function h(e){w=i(w,e)}function d(){if(!d.called){d.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,p)}}function m(){addEventListener("DOMContentLoaded",d,!1),addEventListener("load",d,!1)}function v(r,t){var n=y[r]=t(e);n.aliases&&n.aliases.forEach(function(e){C[e]=r})}function N(){return Object.keys(y)}function E(e){return e=(e||"").toLowerCase(),y[e]||y[C[e]]}var w={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},y={},C={};return e.highlight=l,e.highlightAuto=f,e.fixMarkup=b,e.highlightBlock=p,e.configure=h,e.initHighlighting=d,e.initHighlightingOnLoad=m,e.registerLanguage=v,e.listLanguages=N,e.getLanguage=E,e.inherit=i,e.IR="[a-zA-Z]\\w*",e.UIR="[a-zA-Z_]\\w*",e.NR="\\b\\d+(\\.\\d+)?",e.CNR="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",e.BNR="\\b(0b[01]+)",e.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",e.BE={b:"\\\\[\\s\\S]",r:0},e.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[e.BE]},e.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[e.BE]},e.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/},e.C=function(r,t,n){var a=e.inherit({cN:"comment",b:r,e:t,c:[]},n||{});return a.c.push(e.PWM),a.c.push({cN:"doctag",b:"(?:TODO|FIXME|NOTE|BUG|XXX):",r:0}),a},e.CLCM=e.C("//","$"),e.CBCM=e.C("/\\*","\\*/"),e.HCM=e.C("#","$"),e.NM={cN:"number",b:e.NR,r:0},e.CNM={cN:"number",b:e.CNR,r:0},e.BNM={cN:"number",b:e.BNR,r:0},e.CSSNM={cN:"number",b:e.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},e.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[e.BE,{b:/\[/,e:/\]/,r:0,c:[e.BE]}]},e.TM={cN:"title",b:e.IR,r:0},e.UTM={cN:"title",b:e.UIR,r:0},e}),hljs.registerLanguage("json",function(e){var r={literal:"true false null"},t=[e.QSM,e.CNM],n={e:",",eW:!0,eE:!0,c:t,k:r},a={b:"{",e:"}",c:[{cN:"attr",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:n}],i:"\\S"},c={b:"\\[",e:"\\]",c:[e.inherit(n)],i:"\\S"};return t.splice(t.length,0,a,c),{c:t,k:r,i:"\\S"}}),hljs.registerLanguage("xml",function(e){var r="[A-Za-z0-9\\._:-]+",t={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php"},n={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"meta",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},e.C("",{r:10}),{b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{name:"style"},c:[n],starts:{e:"",rE:!0,sL:["css","xml"]}},{cN:"tag",b:"|$)",e:">",k:{name:"script"},c:[n],starts:{e:"",rE:!0,sL:["actionscript","javascript","handlebars","xml"]}},t,{cN:"meta",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"name",b:/[^\/><\s]+/,r:0},n]}]}}),hljs.registerLanguage("javascript",function(e){return{aliases:["js"],k:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await import from as",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"},c:[{cN:"meta",r:10,b:/^\s*['"]use (strict|asm)['"]/},{cN:"meta",b:/^#!/,e:/$/},e.ASM,e.QSM,{cN:"string",b:"`",e:"`",c:[e.BE,{cN:"subst",b:"\\$\\{",e:"\\}"}]},e.CLCM,e.CBCM,{cN:"number",v:[{b:"\\b(0[bB][01]+)"},{b:"\\b(0[oO][0-7]+)"},{b:e.CNR}],r:0},{b:"("+e.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[e.CLCM,e.CBCM,e.RM,{b:/\s*[);\]]/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[e.inherit(e.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,eB:!0,eE:!0,c:[e.CLCM,e.CBCM]}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+e.IR,r:0},{cN:"class",bK:"class",e:/[{;=]/,eE:!0,i:/[:"\[\]]/,c:[{bK:"extends"},e.UTM]},{bK:"constructor",e:/\{/,eE:!0}],i:/#(?!!)/}}),hljs.registerLanguage("css",function(e){var r="[a-zA-Z-][a-zA-Z0-9_-]*",t={b:/[A-Z\_\.\-]+\s*:/,rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:/\S/,e:":",eE:!0,starts:{eW:!0,eE:!0,c:[{b:/[\w-]+\s*\(/,rB:!0,c:[{cN:"built_in",b:/[\w-]+/}]},e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"number",b:"#[0-9A-Fa-f]+"},{cN:"meta",b:"!important"}]}}]};return{cI:!0,i:/[=\/|'\$]/,c:[e.CBCM,{cN:"selector-id",b:/#[A-Za-z0-9_-]+/},{cN:"selector-class",b:/\.[A-Za-z0-9_-]+/},{cN:"selector-attr",b:/\[/,e:/\]/,i:"$"},{cN:"selector-pseudo",b:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[e.ASM,e.QSM,e.CSSNM]}]},{cN:"selector-tag",b:r,r:0},{b:"{",e:"}",i:/\S/,c:[e.CBCM,t]}]}}); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /models/book.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @license 3 | * Copyright 2018 Cayle Sharrock 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 11 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | * License for the specific language governing permissions and limitations under the License. 13 | * 14 | */ 15 | 16 | const Sequelize = require('sequelize'); 17 | const sequelize = require('./connection'); 18 | const BigNumber = require('bignumber.js'); 19 | const JournalEntry = require('./journal'); 20 | const Transaction = require('./transaction'); 21 | const {ZERO} = require('./types'); 22 | const {AleError, codes} = require('../lib/errors'); 23 | const Op = Sequelize.Op; 24 | 25 | /** 26 | * A Book contains many journal entries 27 | */ 28 | const Book = sequelize.define('book', { 29 | name: {type: Sequelize.TEXT, unique: true}, 30 | quoteCurrency: {type: Sequelize.STRING, defaultValue: 'USD'} 31 | }); 32 | 33 | JournalEntry.belongsTo(Book, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}); 34 | Transaction.belongsTo(Book, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}); // Redundant, but saves double JOINS in a few queries 35 | Book.JournalEntries = Book.hasMany(JournalEntry, {foreignKey: {allowNull: false}, onDelete: 'CASCADE'}); 36 | 37 | /** 38 | * Creates a new journal entry, but doesn't persist it to the DB until entry.commit() is called 39 | */ 40 | Book.prototype.newJournalEntry = function(memo, date = null) { 41 | return JournalEntry.build({ 42 | bookId: this.id, 43 | memo: memo, 44 | quoteCurrency: this.quoteCurrency, 45 | timestamp: date || new Date() 46 | }); 47 | }; 48 | 49 | /** 50 | * Returns journal entries matching the given constraints 51 | * @param query 52 | * @param query.startDate {string|number} 53 | * @param query.endDate {string|number} 54 | * @param query.memo {string} 55 | * @param query.newestFirst {boolean} Order results by desc timestamp, (default : false). 56 | * @return Promise 57 | */ 58 | Book.prototype.getJournalEntries = function(query) { 59 | const parsedQuery = parseQuery(this.getDataValue('id'), query); 60 | parsedQuery.order = parsedQuery.order || [['timestamp', 'ASC']]; 61 | return JournalEntry.findAll(parsedQuery).then(rows => { 62 | const results = rows.map(r => r.values()); 63 | return results; 64 | }).catch(e => { 65 | const err = new AleError(`JournalEntry query failed. ${e.message}`, codes.DatabaseQueryError); 66 | return sequelize.Promise.reject(err); 67 | }); 68 | }; 69 | /** 70 | * Returns a promise fo the balance of the given account. 71 | * @param query 72 | * @param query.account {string|Array} [acct, subacct, subsubacct] 73 | * @param query.startDate {string|number} Anything parseable by new Date() 74 | * @param query.endDate {string|number} Anything parseable by new Date() 75 | * @param query.perPage {number} Limit results to perPage 76 | * @param query.page {number} Return page number 77 | * @param inQuoteCurrency boolean - whether to convert balance to the quote currency or not (default: false) 78 | * @return {creditTotal, debitTotal, balance, currency, numTransactions} 79 | */ 80 | Book.prototype.getBalance = function(query, inQuoteCurrency = false) { 81 | query = parseQuery(this.getDataValue('id'), query); 82 | const credit = inQuoteCurrency ? sequelize.literal('credit * "exchangeRate"') : sequelize.col('credit'); 83 | const debit = inQuoteCurrency ? sequelize.literal('debit * "exchangeRate"') : sequelize.col('debit'); 84 | query.attributes = [ 85 | [sequelize.fn('SUM', credit), 'creditTotal'], 86 | [sequelize.fn('SUM', debit), 'debitTotal'], 87 | [sequelize.fn('COUNT', sequelize.col('id')), 'numTransactions'], 88 | [sequelize.fn('MAX', sequelize.col('currency')), 'currency'] 89 | ]; 90 | return Transaction.findAll(query).then(result => { 91 | result = result.shift(); 92 | if (!result) { 93 | return { 94 | balance: 0, 95 | notes: 0 96 | }; 97 | } 98 | const creditTotal = +result.get('creditTotal'); 99 | const debitTotal = +result.get('debitTotal'); 100 | const total = creditTotal - debitTotal; 101 | return { 102 | creditTotal: creditTotal, 103 | debitTotal: debitTotal, 104 | balance: total, 105 | currency: inQuoteCurrency ? this.quoteCurrency : result.get('currency'), 106 | numTransactions: +result.get('numTransactions') 107 | }; 108 | }); 109 | }; 110 | 111 | /** 112 | * Return all journal entries ordered by time for a given book (subject to the constraints passed in the query) 113 | * @param query 114 | * @param query.startDate {string|number} Anything parseable by new Date() 115 | * @param query.endDate {string|number} Anything parseable by new Date() 116 | * @param query.perPage {number} Limit results to perPage 117 | * @param query.page {number} Return page number 118 | * @param query.newestFirst {boolean} Order results by desc timestamp, (default : false). 119 | * @return {Array} of JournalEntry 120 | */ 121 | Book.prototype.getLedger = function(query) { 122 | query = parseQuery(this.get('id'), query); 123 | query.order = query.order || [['timestamp', 'ASC']]; 124 | query.include = [ Transaction ]; 125 | return JournalEntry.findAll(query); 126 | }; 127 | 128 | /** 129 | * Return all transactions ordered by time for a given book (subject to the constraints passed in the query) 130 | * @param query 131 | * @param query.account {string|Array} A single, or array of accounts to match. Assets will match Assets and Assets:* 132 | * @param query.perPage {number} Limit results to perPage 133 | * @param query.page {number} Return page number 134 | * @param query.newestFirst {boolean} Order results by desc timestamp, (default : false). 135 | * @return {Array} of Transaction 136 | */ 137 | Book.prototype.getTransactions = function(query) { 138 | query = parseQuery(this.get('id'), query); 139 | query.order = query.order || [['timestamp', 'ASC']]; 140 | return Transaction.findAll(query); 141 | }; 142 | 143 | Book.prototype.voidEntry = function(journalId, reason) { 144 | return JournalEntry.findById(journalId) 145 | .then(entry => { 146 | if (!entry) { 147 | return sequelize.Promise.reject(new AleError(`Journal entry not found with ID ${journalId}`, codes.TransactionIDNotFound)); 148 | } 149 | return entry.voidEntry(this, reason); 150 | }); 151 | }; 152 | 153 | Book.prototype.listAccounts = function() { 154 | return Transaction.aggregate('account', 'distinct', { 155 | where: { 156 | bookId: this.getDataValue('id') 157 | }, 158 | plain: false 159 | }).then(results => { 160 | // Make array 161 | const final = []; 162 | for (let result of results) { 163 | const paths = result.distinct.split(':'); 164 | const prev = []; 165 | for (let acct of paths) { 166 | prev.push(acct); 167 | final.push(prev.join(':')); 168 | } 169 | } 170 | return Array.from(new Set(final)); // uniques 171 | }); 172 | }; 173 | 174 | Book.prototype.markToMarket = function(query, exchangeRates) { 175 | const rates = Book.normalizeRates(this.quoteCurrency, exchangeRates); 176 | if (!rates) { 177 | const err = new AleError('Cannot mark-to-market if no current exchange rates are supplied', codes.MissingInput); 178 | return sequelize.Promise.reject(err); 179 | } 180 | query = parseQuery(this.getDataValue('id'), query); 181 | query.attributes = [ 182 | 'account', 183 | 'currency', 184 | [sequelize.fn('SUM', sequelize.literal('credit - debit')), 'balance'] 185 | ]; 186 | query.group = ['account', 'currency']; 187 | return Transaction.findAll(query).then(txs => { 188 | const result = {}; 189 | let profit = ZERO; 190 | txs.forEach(tx => { 191 | if (!rates[tx.currency]) { 192 | throw new AleError(`A ${tx.currency} transaction exists, but its current exchange rate was not provided`, codes.ExchangeRateNotFound); 193 | } 194 | let currentBal = (new BigNumber(tx.get('balance'))).div(rates[tx.currency]); 195 | profit = profit.plus(currentBal); 196 | result[tx.get('account')] = +currentBal; 197 | }); 198 | result.unrealizedProfit = +profit; 199 | return result; 200 | }).catch(err => { 201 | return sequelize.Promise.reject(err); 202 | }); 203 | }; 204 | 205 | Book.normalizeRates = function(currency, rates) { 206 | let base = rates[currency]; 207 | if (!base) { 208 | return null; 209 | } 210 | base = new BigNumber(base); 211 | const result = Object.assign({}, rates); 212 | for (let cur in result) { 213 | result[cur] = +(new BigNumber(rates[cur]).div(base)); 214 | } 215 | return result; 216 | }; 217 | 218 | /** 219 | * Convert object into Sequelise 'where' clause 220 | * @param query {{account: {acct, subacct, subsubacct}, startDate, endDate, perPage, page, memo}} 221 | * @returns {Array} of Book models 222 | */ 223 | function parseQuery(id, query) { 224 | let account; 225 | const parsed = {where: {bookId: id}}; 226 | query = query || {}; 227 | if (query.perPage) { 228 | const perPage = query.perPage || 25; 229 | parsed.offset = ((query.page || 1) - 1) * perPage; 230 | parsed.limit = query.perPage; 231 | delete query.perPage; 232 | delete query.page; 233 | } 234 | 235 | if ((account = query.account)) { 236 | 237 | if (account instanceof Array) { 238 | let accountList = account.map(a => ({[Op.like]: `${a}%`})); 239 | parsed.where.account = {[Op.or]: accountList}; 240 | } 241 | else { 242 | parsed.where.account = {[Op.like]: `${account}%`}; 243 | } 244 | delete query.account; 245 | } 246 | 247 | if (query.journalEntry) { 248 | parsed.where.journalEntry = query.journalEntry; 249 | } 250 | 251 | if (query.startDate || query.endDate) { 252 | parsed.where.timestamp = {}; 253 | } 254 | if (query.startDate) { 255 | parsed.where.timestamp[Op.gte] = new Date(query.startDate); 256 | delete query.startDate; 257 | } 258 | if (query.endDate) { 259 | parsed.where.timestamp[Op.lte] = new Date(query.endDate); 260 | delete query.endDate; 261 | } 262 | if (query.memo) { 263 | parsed.where.memo = {[Op.or]: [query.memo, `${query.memo} [REVERSED]`]}; 264 | delete query.memo; 265 | } 266 | if (query.newestFirst) { 267 | parsed.order = [['timestamp', 'DESC']]; 268 | } 269 | 270 | return parsed; 271 | } 272 | 273 | Book.listBooks = function() { 274 | return Book.findAll({order: ['name']}).then(results => { 275 | if (!results) { 276 | return []; 277 | } 278 | return results; 279 | }); 280 | }; 281 | 282 | /** 283 | * Gets an existing book, or creates a new one 284 | * @param name The name of the new book 285 | * @param quoteCurrency The Base currency for the book. If the book already exists, this parameter must match the existing base currency or be undefined. 286 | */ 287 | Book.getOrCreateBook = function(name, quoteCurrency) { 288 | return Book.findOrCreate({where: {name: name}, defaults: {quoteCurrency: quoteCurrency}}) 289 | .then(result => { 290 | if (!result || result.length != 2) { 291 | return sequelize.Promise.reject(new AleError('Book query failed to return expected result', codes.DatabaseQueryError)); 292 | } 293 | const book = result[0]; 294 | const cur = book.quoteCurrency; 295 | const isNewBook = result[1]; 296 | if (quoteCurrency && (quoteCurrency != cur)) { 297 | const err = new AleError(`Request Base currency does not match existing base currency. Requested: ${quoteCurrency}. Current: ${cur}`, codes.MismatchedCurrency); 298 | return sequelize.Promise.reject(err); 299 | } 300 | return {isNew: isNewBook, book: book}; 301 | }); 302 | }; 303 | 304 | /** 305 | * Gets an existing, or returns an error 306 | * @param name The name of the new book 307 | */ 308 | Book.getBook = function(name) { 309 | return Book.findOne({where: {name: name}}).then(book => { 310 | if (!book) { 311 | return sequelize.Promise.reject(new AleError(`Book ${name} does not exist.`, codes.BookDoesNotExist)); 312 | } 313 | return book; 314 | }, err => { 315 | return sequelize.Promise.reject(new AleError(`Error getting book info. ${err.message}`, codes.DatabaseQueryError)); 316 | }); 317 | }; 318 | 319 | Book.prototype.values = function() { 320 | return { 321 | id: this.get('id'), 322 | name: this.get('name'), 323 | currency: this.get('quoteCurrency'), 324 | createdAt: this.get('createdAt').valueOf(), 325 | updatedAt: this.get('updatedAt').valueOf() 326 | }; 327 | }; 328 | 329 | module.exports = Book; 330 | -------------------------------------------------------------------------------- /web_deploy/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: '2.0' 2 | schemes: 3 | - http 4 | host: 'localhost:8813' 5 | basePath: / 6 | info: 7 | description: | 8 | # A.L.E Double-Entry Accounting REST API 9 | 10 | ALE divides itself into "books", each of which store *journal entries* and their child *transactions*. 11 | The cardinal rule of double-entry accounting is that "everything must balance out to zero", and that rule is applied 12 | to every journal entry written to the book. 13 | version: 1.0.0 14 | title: A.L.E. 15 | license: 16 | name: Apache 2.0 17 | url: 'http://www.apache.org/licenses/LICENSE-2.0.html' 18 | x-logo: 19 | url: /ale/logo.png 20 | externalDocs: 21 | description: A.L.E. Github page 22 | url: 'https://github.com/CjS77/ale' 23 | produces: 24 | - application/json 25 | consumes: 26 | - application/json 27 | paths: 28 | /books/: 29 | get: 30 | summary: List all current books 31 | description: | 32 | Produces an array of all the books currently registered on the database. 33 | operationId: getBooks 34 | responses: 35 | '200': 36 | description: The array of books 37 | schema: 38 | $ref: '#/definitions/Books' 39 | examples: 40 | application/json: 41 | - id: General Ledger 42 | currency: USD 43 | - id: London office ledger 44 | currency: GBP 45 | post: 46 | parameters: 47 | - in: body 48 | name: body 49 | description: New Book definition 50 | required: true 51 | schema: 52 | $ref: '#/definitions/Book' 53 | operationId: postBooks 54 | responses: 55 | '200': 56 | description: Success 57 | schema: 58 | $ref: '#/definitions/BookResponse' 59 | examples: 60 | application/json: 61 | success: false 62 | message: Book 'General' already exists 63 | '400': 64 | description: Bad input 65 | schema: 66 | $ref: '#/definitions/ErrorResponse' 67 | examples: 68 | application/json: 69 | success: false 70 | message: Book 'Foobar' does not exist 71 | '/books/{bookId}/ledger': 72 | parameters: 73 | - name: bookId 74 | description: The book to extract entries from 75 | in: path 76 | type: integer 77 | required: true 78 | post: 79 | summary: Post a new Journal entry 80 | description: Add a new (balanced) Journal entry to the book 81 | operationId: postBookEntry 82 | parameters: 83 | - name: body 84 | in: body 85 | description: The new Journal Entry definition 86 | schema: 87 | $ref: '#/definitions/NewJournalEntry' 88 | produces: 89 | - application/json 90 | responses: 91 | '200': 92 | description: Success 93 | schema: 94 | $ref: '#/definitions/PostEntryResponse' 95 | get: 96 | summary: Fetch the ledger 97 | description: | 98 | Fetches all transactions for the given book for the dates provided 99 | operationId: getBookEntries 100 | parameters: 101 | - name: bookId 102 | description: The book to extract entries from 103 | in: path 104 | type: integer 105 | required: true 106 | - name: startDate 107 | in: query 108 | description: The start date for entries 109 | required: false 110 | type: string 111 | - name: endDate 112 | in: query 113 | description: The end date for entries 114 | required: false 115 | type: string 116 | - name: perPage 117 | in: query 118 | description: The number of results per page 119 | required: false 120 | type: integer 121 | - name: page 122 | in: query 123 | description: The page number 124 | required: false 125 | type: integer 126 | produces: 127 | - application/json 128 | responses: 129 | '200': 130 | description: Success 131 | schema: 132 | $ref: '#/definitions/Entries' 133 | examples: 134 | application/json: 135 | book: 136 | id: General Ledger 137 | currency: USD 138 | startDate: 1520228313023 139 | endDate: 1520428313023 140 | entries: 141 | - date: 1520228313023 142 | memo: Payroll 143 | transactions: 144 | - account: 'Bank:Local' 145 | debit: 1000 146 | currency: USD 147 | exchangeRate: 1 148 | - account: 'Employees:Alice' 149 | value: 500 150 | currency: USD 151 | exchangeRate: 1 152 | - account: 'Employees:Bob' 153 | value: 500 154 | currency: USD 155 | exchangeRate: 1 156 | '400': 157 | description: 'Invalid input, such as unknown book' 158 | schema: 159 | $ref: '#/definitions/Response' 160 | '/books/{bookId}/accounts': 161 | parameters: 162 | - name: bookId 163 | description: The book to extract entries from 164 | in: path 165 | type: integer 166 | required: true 167 | get: 168 | summary: List all accounts 169 | operationId: getAccounts 170 | produces: 171 | - application/json 172 | responses: 173 | '200': 174 | description: Success 175 | schema: 176 | type: array 177 | items: 178 | type: string 179 | '/books/{bookId}/transactions': 180 | parameters: 181 | - name: bookId 182 | description: The book to extract entries from 183 | in: path 184 | type: integer 185 | required: true 186 | get: 187 | summary: List all transactions for given accounts 188 | operationId: getTransactions 189 | parameters: 190 | - name: accounts 191 | description: A comma-separated search term for accounts 192 | in: query 193 | type: string 194 | required: true 195 | - name: perPage 196 | in: query 197 | description: The number of results per page 198 | required: false 199 | type: integer 200 | - name: page 201 | in: query 202 | description: The page number 203 | required: false 204 | type: integer 205 | produces: 206 | - application/json 207 | responses: 208 | '200': 209 | description: Success 210 | schema: 211 | type: array 212 | items: 213 | $ref: '#/definitions/Transaction' 214 | '/books/{bookId}/balance': 215 | parameters: 216 | - name: bookId 217 | description: The book to extract entries from 218 | in: path 219 | type: integer 220 | required: true 221 | get: 222 | summary: Return an account balance 223 | operationId: getBalance 224 | parameters: 225 | - name: account 226 | description: The account to get the balance for 227 | in: query 228 | type: string 229 | required: true 230 | - name: inQuoteCurrency 231 | description: 'If true (default), converts all values to the quote currency first' 232 | in: query 233 | type: boolean 234 | required: false 235 | produces: 236 | - application/json 237 | responses: 238 | '200': 239 | description: Success 240 | schema: 241 | $ref: '#/definitions/Balance' 242 | '/books/{bookId}/marktomarket': 243 | parameters: 244 | - name: bookId 245 | description: The book to extract entries from 246 | in: path 247 | type: integer 248 | required: true 249 | post: 250 | summary: Mark the account(s) to market 251 | description: Calculates the unlrealised profit of the given accounts at the exchange rate vector provided 252 | operationId: postMarkToMarket 253 | parameters: 254 | - name: body 255 | in: body 256 | schema: 257 | type: object 258 | required: 259 | - accounts 260 | - exchangeRates 261 | properties: 262 | accounts: 263 | type: array 264 | items: 265 | type: string 266 | exchangeRates: 267 | type: object 268 | additionalProperties: 269 | type: number 270 | produces: 271 | - application/json 272 | responses: 273 | '200': 274 | description: Success 275 | schema: 276 | type: object 277 | items: 278 | $ref: '#/definitions/UnrealisedProfit' 279 | definitions: 280 | Books: 281 | type: array 282 | items: 283 | $ref: '#/definitions/Book' 284 | Book: 285 | type: object 286 | required: 287 | - name 288 | - currency 289 | properties: 290 | id: 291 | description: The id for the book the book 292 | type: integer 293 | name: 294 | description: The name of the book 295 | type: string 296 | example: General Ledger 297 | currency: 298 | description: | 299 | The currency the book is referenced in. 300 | All other currencies and calculations are quoted in terms of this currency 301 | type: string 302 | example: USD 303 | createdAt: 304 | description: The timestamp of when the book was created 305 | type: number 306 | updatedAt: 307 | description: The timestamp of the last time this entry was modified 308 | type: number 309 | Entries: 310 | type: object 311 | properties: 312 | book: 313 | $ref: '#/definitions/Book' 314 | startDate: 315 | type: number 316 | endDate: 317 | type: number 318 | entries: 319 | type: array 320 | items: 321 | $ref: '#/definitions/Entry' 322 | Entry: 323 | type: object 324 | properties: 325 | date: 326 | description: The timestamp for the entry 327 | type: number 328 | memo: 329 | description: The description for the entry 330 | type: string 331 | voided: 332 | description: Indicates whether the entry is still valid 333 | type: boolean 334 | voidReason: 335 | description: The reason for the entry reversal 336 | type: string 337 | transactions: 338 | description: An array of transactions for the entry 339 | type: array 340 | items: 341 | $ref: '#/definitions/Transaction' 342 | Transaction: 343 | type: object 344 | required: 345 | - account 346 | properties: 347 | account: 348 | description: The account this transaction is reflected on 349 | type: string 350 | credit: 351 | description: The credit value of the transaction 352 | type: number 353 | debit: 354 | description: The debit value of the transaction 355 | type: number 356 | currency: 357 | description: The currency for this transaction 358 | type: string 359 | exchangeRate: 360 | description: The exchange rate to convert to the basis currency 361 | type: number 362 | Response: 363 | type: object 364 | properties: 365 | success: 366 | description: Indicates whether request was succesful 367 | type: boolean 368 | message: 369 | type: string 370 | ErrorResponse: 371 | allOf: 372 | - $ref: '#/definitions/Response' 373 | - properties: 374 | errorCode: 375 | type: integer 376 | BookResponse: 377 | allOf: 378 | - $ref: '#/definitions/Response' 379 | - $ref: '#/definitions/Book' 380 | PostEntryResponse: 381 | allOf: 382 | - $ref: '#/definitions/Response' 383 | - properties: 384 | id: 385 | description: The id of the new Journal Entry 386 | type: integer 387 | NewJournalEntry: 388 | type: object 389 | required: 390 | - memo 391 | - transactions 392 | properties: 393 | memo: 394 | description: The Journal Entry description 395 | type: string 396 | timestamp: 397 | description: The time stamp for the journal entry 398 | type: string 399 | transactions: 400 | type: array 401 | items: 402 | $ref: '#/definitions/Transaction' 403 | Balance: 404 | type: object 405 | properties: 406 | creditTotal: 407 | description: The total value of credits into the account 408 | type: number 409 | debitTotal: 410 | description: The total value of debits into the account 411 | type: number 412 | balance: 413 | description: The current account balabce (credits - debits) 414 | type: number 415 | currency: 416 | description: The nominal currency of the account 417 | type: number 418 | numTransactions: 419 | description: The number of transactions into the account 420 | type: number 421 | UnrealisedProfit: 422 | type: object 423 | properties: 424 | unrealizedProfit: 425 | description: The total current unrealised profit for the given accounts 426 | type: number 427 | additionalProperties: 428 | type: number 429 | -------------------------------------------------------------------------------- /spec/swagger.yaml: -------------------------------------------------------------------------------- 1 | # Documentation: https://swagger.io/docs/specification/2-0/basic-structure/ 2 | 3 | swagger: '2.0' 4 | schemes: 5 | - http 6 | host: localhost:8813 7 | basePath: / 8 | info: 9 | description: | 10 | # A.L.E Double-Entry Accounting REST API 11 | 12 | ALE divides itself into "books", each of which store *journal entries* and their child *transactions*. 13 | The cardinal rule of double-entry accounting is that "everything must balance out to zero", and that rule is applied 14 | to every journal entry written to the book. 15 | version: '1.0.0' 16 | title: A.L.E. 17 | license: 18 | name: Apache 2.0 19 | url: 'http://www.apache.org/licenses/LICENSE-2.0.html' 20 | x-logo: 21 | url: '/ale/logo.png' 22 | externalDocs: 23 | description: A.L.E. Github page 24 | url: 'https://github.com/CjS77/ale' 25 | produces: 26 | # This is a global default. You can OVERWRITE it in each specific operation. 27 | - application/json 28 | consumes: 29 | # List of mime types the API endpoints consume 30 | # This is a global default. You can OVERWRITE it in each specific operation. 31 | - application/json 32 | # Holds the relative paths to the individual endpoints. The path is appended to the basePath in order to construct the full URL. 33 | paths: 34 | '/books/': # path parameter in curly braces 35 | get: 36 | summary: List all current books 37 | description: | 38 | Produces an array of all the books currently registered on the database. 39 | operationId: getBooks 40 | responses: 41 | 200: 42 | description: The array of books 43 | schema: # response schema can be specified for each response 44 | $ref: '#/definitions/Books' 45 | examples: 46 | application/json: 47 | [ 48 | { id: "General Ledger", currency: "USD"}, 49 | { id: "London office ledger", currency: "GBP"}, 50 | ] 51 | 52 | post: 53 | parameters: 54 | - in: body 55 | name: body 56 | description: New Book definition 57 | required: true 58 | schema: 59 | $ref: '#/definitions/Book' 60 | operationId: postBooks 61 | responses: 62 | 200: 63 | description: Success 64 | schema: 65 | $ref: '#/definitions/BookResponse' 66 | examples: 67 | application/json: 68 | success: false 69 | message: Book 'General' already exists 70 | 400: 71 | description: Bad input 72 | schema: 73 | $ref: '#/definitions/ErrorResponse' 74 | examples: 75 | application/json: 76 | success: false 77 | message: Book 'Foobar' does not exist 78 | 79 | '/books/{bookId}/ledger': 80 | parameters: 81 | - name: bookId 82 | description: The book to extract entries from 83 | in: path 84 | type: integer 85 | required: true 86 | post: 87 | summary: Post a new Journal entry 88 | description: Add a new (balanced) Journal entry to the book 89 | operationId: postBookEntry 90 | 91 | parameters: 92 | - name: body 93 | in: body 94 | description: The new Journal Entry definition 95 | schema: 96 | $ref: '#/definitions/NewJournalEntry' 97 | 98 | produces: 99 | - application/json 100 | responses: # list of responses 101 | 200: 102 | description: Success 103 | schema: 104 | $ref: '#/definitions/PostEntryResponse' 105 | 106 | get: 107 | summary: Fetch the ledger 108 | description: | 109 | Fetches all transactions for the given book for the dates provided 110 | operationId: getBookEntries 111 | 112 | parameters: 113 | - name: bookId 114 | description: The book to extract entries from 115 | in: path 116 | type: integer 117 | required: true 118 | 119 | - name: startDate 120 | in: query 121 | description: 'The start date for entries' 122 | required: false 123 | type: string 124 | 125 | - name: endDate 126 | in: query 127 | description: 'The end date for entries' 128 | required: false 129 | type: string 130 | 131 | - name: perPage 132 | in: query 133 | description: 'The number of results per page' 134 | required: false 135 | type: integer 136 | 137 | - name: page 138 | in: query 139 | description: 'The page number' 140 | required: false 141 | type: integer 142 | 143 | produces: 144 | - application/json 145 | responses: # list of responses 146 | 200: 147 | description: Success 148 | schema: 149 | $ref: '#/definitions/Entries' 150 | examples: 151 | application/json: 152 | { 153 | book: { id: "General Ledger", currency: 'USD' }, 154 | startDate: 1520228313023, 155 | endDate: 1520428313023, 156 | entries: [ 157 | { 158 | date: 1520228313023, 159 | memo: 'Payroll', 160 | transactions: [ 161 | { account: "Bank:Local", debit: 1000.0, currency: "USD", exchangeRate: 1.0 }, 162 | { account: "Employees:Alice", value: 500.0, currency: "USD", exchangeRate: 1.0 }, 163 | { account: "Employees:Bob", value: 500.0, currency: "USD", exchangeRate: 1.0 } 164 | ] 165 | } 166 | ] 167 | } 168 | 400: 169 | description: Invalid input, such as unknown book 170 | schema: 171 | $ref: '#/definitions/Response' 172 | 173 | '/books/{bookId}/accounts': 174 | parameters: 175 | - name: bookId 176 | description: The book to extract entries from 177 | in: path 178 | type: integer 179 | required: true 180 | get: 181 | summary: List all accounts 182 | operationId: getAccounts 183 | produces: 184 | - application/json 185 | responses: # list of responses 186 | 200: 187 | description: Success 188 | schema: 189 | type: array 190 | items: 191 | type: string 192 | 193 | '/books/{bookId}/transactions': 194 | parameters: 195 | - name: bookId 196 | description: The book to extract entries from 197 | in: path 198 | type: integer 199 | required: true 200 | get: 201 | summary: List all transactions for given accounts 202 | operationId: getTransactions 203 | parameters: 204 | - name: accounts 205 | description: A comma-separated search term for accounts 206 | in: query 207 | type: string 208 | required: true 209 | 210 | - name: perPage 211 | in: query 212 | description: 'The number of results per page' 213 | required: false 214 | type: integer 215 | 216 | - name: page 217 | in: query 218 | description: 'The page number' 219 | required: false 220 | type: integer 221 | 222 | produces: 223 | - application/json 224 | responses: # list of responses 225 | 200: 226 | description: Success 227 | schema: 228 | type: array 229 | items: 230 | $ref: '#/definitions/Transaction' 231 | 232 | '/books/{bookId}/balance': 233 | parameters: 234 | - name: bookId 235 | description: The book to extract entries from 236 | in: path 237 | type: integer 238 | required: true 239 | get: 240 | summary: Return an account balance 241 | operationId: getBalance 242 | parameters: 243 | - name: account 244 | description: The account to get the balance for 245 | in: query 246 | type: string 247 | required: true 248 | - name: inQuoteCurrency 249 | description: If true (default), converts all values to the quote currency first 250 | in: query 251 | type: boolean 252 | required: false 253 | produces: 254 | - application/json 255 | responses: # list of responses 256 | 200: 257 | description: Success 258 | schema: 259 | $ref: '#/definitions/Balance' 260 | 261 | '/books/{bookId}/marktomarket': 262 | parameters: 263 | - name: bookId 264 | description: The book to extract entries from 265 | in: path 266 | type: integer 267 | required: true 268 | post: 269 | summary: Mark the account(s) to market 270 | description: Calculates the unlrealised profit of the given accounts at the exchange rate vector provided 271 | operationId: postMarkToMarket 272 | parameters: 273 | - name: body 274 | in: body 275 | schema: 276 | type: object 277 | required: 278 | - accounts 279 | - exchangeRates 280 | properties: 281 | accounts: 282 | type: array 283 | items: 284 | type: string 285 | exchangeRates: 286 | type: object 287 | additionalProperties: 288 | type: number 289 | produces: 290 | - application/json 291 | responses: # list of responses 292 | 200: 293 | description: Success 294 | schema: 295 | type: object 296 | items: 297 | $ref: '#/definitions/UnrealisedProfit' 298 | 299 | # An object to hold data types that can be consumed and produced by operations. 300 | # These data types can be primitives, arrays or models. 301 | definitions: 302 | 303 | Books: 304 | type: array 305 | items: 306 | $ref: '#/definitions/Book' 307 | 308 | Book: 309 | type: object 310 | required: 311 | - name 312 | - currency 313 | properties: 314 | id: 315 | description: The id for the book the book 316 | type: integer 317 | name: 318 | description: The name of the book 319 | type: string 320 | example: General Ledger 321 | currency: 322 | description: | 323 | The currency the book is referenced in. 324 | All other currencies and calculations are quoted in terms of this currency 325 | type: string 326 | example: USD 327 | createdAt: 328 | description: The timestamp of when the book was created 329 | type: number 330 | updatedAt: 331 | description: The timestamp of the last time this entry was modified 332 | type: number 333 | 334 | Entries: 335 | type: object 336 | properties: 337 | book: 338 | $ref: '#/definitions/Book' 339 | startDate: 340 | type: number 341 | endDate: 342 | type: number 343 | entries: 344 | type: array 345 | items: 346 | $ref: '#/definitions/Entry' 347 | 348 | Entry: 349 | type: object 350 | properties: 351 | date: 352 | description: The timestamp for the entry 353 | type: number 354 | memo: 355 | description: The description for the entry 356 | type: string 357 | voided: 358 | description: Indicates whether the entry is still valid 359 | type: boolean 360 | voidReason: 361 | description: The reason for the entry reversal 362 | type: string 363 | transactions: 364 | description: An array of transactions for the entry 365 | type: array 366 | items: 367 | $ref: '#/definitions/Transaction' 368 | 369 | Transaction: 370 | type: object 371 | required: 372 | - account 373 | properties: 374 | account: 375 | description: The account this transaction is reflected on 376 | type: string 377 | credit: 378 | description: The credit value of the transaction 379 | type: number 380 | debit: 381 | description: The debit value of the transaction 382 | type: number 383 | currency: 384 | description: The currency for this transaction 385 | type: string 386 | exchangeRate: 387 | description: The exchange rate to convert to the basis currency 388 | type: number 389 | 390 | Response: 391 | type: object 392 | properties: 393 | success: 394 | description: Indicates whether request was succesful 395 | type: boolean 396 | message: 397 | type: string 398 | 399 | ErrorResponse: 400 | allOf: 401 | - $ref: '#/definitions/Response' 402 | - properties: 403 | errorCode: 404 | type: integer 405 | 406 | BookResponse: 407 | allOf: 408 | - $ref: '#/definitions/Response' 409 | - $ref: '#/definitions/Book' 410 | 411 | PostEntryResponse: 412 | allOf: 413 | - $ref: '#/definitions/Response' 414 | - properties: 415 | id: 416 | description: The id of the new Journal Entry 417 | type: integer 418 | 419 | NewJournalEntry: 420 | type: object 421 | required: 422 | - memo 423 | - transactions 424 | properties: 425 | memo: 426 | description: The Journal Entry description 427 | type: string 428 | timestamp: 429 | description: The time stamp for the journal entry 430 | type: string 431 | transactions: 432 | type: array 433 | items: 434 | $ref: '#/definitions/Transaction' 435 | 436 | Balance: 437 | type: object 438 | properties: 439 | creditTotal: 440 | description: The total value of credits into the account 441 | type: number 442 | debitTotal: 443 | description: The total value of debits into the account 444 | type: number 445 | balance: 446 | description: The current account balabce (credits - debits) 447 | type: number 448 | currency: 449 | description: The nominal currency of the account 450 | type: number 451 | numTransactions: 452 | description: The number of transactions into the account 453 | type: number 454 | 455 | UnrealisedProfit: 456 | type: object 457 | properties: 458 | unrealizedProfit: 459 | description: The total current unrealised profit for the given accounts 460 | type: number 461 | additionalProperties: 462 | type: number 463 | -------------------------------------------------------------------------------- /web_deploy/swagger-ui/lib/marked.js: -------------------------------------------------------------------------------- 1 | (function(){function e(e){this.tokens=[],this.tokens.links={},this.options=e||a.defaults,this.rules=p.normal,this.options.gfm&&(this.options.tables?this.rules=p.tables:this.rules=p.gfm)}function t(e,t){if(this.options=t||a.defaults,this.links=e,this.rules=u.normal,this.renderer=this.options.renderer||new n,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=u.breaks:this.rules=u.gfm:this.options.pedantic&&(this.rules=u.pedantic)}function n(e){this.options=e||{}}function r(e){this.tokens=[],this.token=null,this.options=e||a.defaults,this.options.renderer=this.options.renderer||new n,this.renderer=this.options.renderer,this.renderer.options=this.options}function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function i(e){return e.replace(/&([#\w]+);/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function l(e,t){return e=e.source,t=t||"",function n(r,s){return r?(s=s.source||s,s=s.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,s),n):new RegExp(e,t)}}function o(){}function h(e){for(var t,n,r=1;rAn error occured:

"+s(c.message+"",!0)+"
";throw c}}var p={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:o,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:o,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:o,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};p.bullet=/(?:[*+-]|\d+\.)/,p.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,p.item=l(p.item,"gm")(/bull/g,p.bullet)(),p.list=l(p.list)(/bull/g,p.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+p.def.source+")")(),p.blockquote=l(p.blockquote)("def",p.def)(),p._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",p.html=l(p.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,p._tag)(),p.paragraph=l(p.paragraph)("hr",p.hr)("heading",p.heading)("lheading",p.lheading)("blockquote",p.blockquote)("tag","<"+p._tag)("def",p.def)(),p.normal=h({},p),p.gfm=h({},p.normal,{fences:/^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,paragraph:/^/}),p.gfm.paragraph=l(p.paragraph)("(?!","(?!"+p.gfm.fences.source.replace("\\1","\\2")+"|"+p.list.source.replace("\\1","\\3")+"|")(),p.tables=h({},p.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),e.rules=p,e.lex=function(t,n){var r=new e(n);return r.lex(t)},e.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},e.prototype.token=function(e,t,n){for(var r,s,i,l,o,h,a,u,c,e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"})),i=this.rules.code.exec(e))e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});else if(i=this.rules.fences.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]});else if(i=this.rules.heading.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});else if(t&&(i=this.rules.nptable.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(i,t,!0),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l=i[2],this.tokens.push({type:"list_start",ordered:l.length>1}),i=i[0].match(this.rules.item),r=!1,c=i.length,u=0;u1&&o.length>1||(e=i.slice(u+1).join("\n")+e,u=c-1)),s=r||/\n\n(?!\s*$)/.test(h),u!==c-1&&(r="\n"===h.charAt(h.length-1),s||(s=r)),this.tokens.push({type:s?"loose_item_start":"list_item_start"}),this.token(h,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(i=this.rules.html.exec(e))e=e.substring(i[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:"pre"===i[1]||"script"===i[1]||"style"===i[1],text:i[0]});else if(!n&&t&&(i=this.rules.def.exec(e)))e=e.substring(i[0].length),this.tokens.links[i[1].toLowerCase()]={href:i[2],title:i[3]};else if(t&&(i=this.rules.table.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:o,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:o,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,u.link=l(u.link)("inside",u._inside)("href",u._href)(),u.reflink=l(u.reflink)("inside",u._inside)(),u.normal=h({},u),u.pedantic=h({},u.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),u.gfm=h({},u.normal,{escape:l(u.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:l(u.text)("]|","~]|")("|","|https?://|")()}),u.breaks=h({},u.gfm,{br:l(u.br)("{2,}","*")(),text:l(u.gfm.text)("{2,}","*")()}),t.rules=u,t.output=function(e,n,r){var s=new t(n,r);return s.output(e)},t.prototype.output=function(e){for(var t,n,r,i,l="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),l+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=s(i[1]),r=n),l+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),l+=this.options.sanitize?s(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,l+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){l+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,l+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),l+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),l+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),l+=this.renderer.codespan(s(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),l+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),l+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),l+=s(this.smartypants(i[0]));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=s(i[1]),r=n,l+=this.renderer.link(r,null,n);return l},t.prototype.outputLink=function(e,t){var n=s(t.href),r=t.title?s(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,s(e[1]))},t.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/--/g,"—").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},t.prototype.mangle=function(e){for(var t,n="",r=e.length,s=0;s.5&&(t="x"+t.toString(16)),n+="&#"+t+";";return n},n.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?'
'+(n?e:s(e,!0))+"\n
\n":"
"+(n?e:s(e,!0))+"\n
"},n.prototype.blockquote=function(e){return"
\n"+e+"
\n"},n.prototype.html=function(e){return e},n.prototype.heading=function(e,t,n){return"'+e+"\n"},n.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},n.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+"\n"},n.prototype.listitem=function(e){return"
  • "+e+"
  • \n"},n.prototype.paragraph=function(e){return"

    "+e+"

    \n"},n.prototype.table=function(e,t){return"\n\n"+e+"\n\n"+t+"\n
    \n"},n.prototype.tablerow=function(e){return"\n"+e+"\n"},n.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+"\n"},n.prototype.strong=function(e){return""+e+""},n.prototype.em=function(e){return""+e+""},n.prototype.codespan=function(e){return""+e+""},n.prototype.br=function(){return this.options.xhtml?"
    ":"
    "},n.prototype.del=function(e){return""+e+""},n.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(i(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(s){return""}if(0===r.indexOf("javascript:"))return""}var l='
    "},n.prototype.image=function(e,t,n){var r=''+n+'":">"},r.parse=function(e,t,n){var s=new r(t,n);return s.parse(e)},r.prototype.parse=function(e){this.inline=new t(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var n="";this.next();)n+=this.tok();return n},r.prototype.next=function(){return this.token=this.tokens.pop()},r.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},r.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},r.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s,i="",l="";for(n="",e=0;e { 23 | let testZAR, testUSD; 24 | before(done => { 25 | testDB.clear().then(() => { 26 | done(); 27 | }); 28 | }); 29 | describe('POST /books', () => { 30 | it('creates a new book', () => { 31 | return request(app) 32 | .post('/books') 33 | .send({name: 'testZAR', currency: 'ZAR'}) 34 | .expect(200) 35 | .then(res => { 36 | const newBook = res.body; 37 | assert.ok(newBook.success); 38 | assert.ok(isFinite(newBook.id)); 39 | assert.equal(newBook.name, 'testZAR'); 40 | assert.equal(newBook.currency, 'ZAR'); 41 | testZAR = newBook.id; 42 | }); 43 | }); 44 | 45 | it('creates another new book', () => { 46 | return request(app) 47 | .post('/books') 48 | .send({name: 'testUSD', currency: 'USD'}) 49 | .expect(200) 50 | .then(res => { 51 | const newBook = res.body; 52 | assert.ok(newBook.success); 53 | assert.ok(isFinite(newBook.id)); 54 | assert.equal(newBook.name, 'testUSD'); 55 | assert.equal(newBook.currency, 'USD'); 56 | testUSD = newBook.id; 57 | }); 58 | }); 59 | 60 | it('returns existing book', () => { 61 | return request(app) 62 | .post('/books') 63 | .send({name: 'testZAR', currency: 'ZAR'}) 64 | .expect(200) 65 | .then(res => { 66 | const newBook = res.body; 67 | assert.equal(newBook.success, false); 68 | assert.equal(newBook.id, testZAR); 69 | assert.equal(newBook.name, 'testZAR'); 70 | assert.equal(newBook.currency, 'ZAR'); 71 | }); 72 | }); 73 | 74 | it('rejects creation of conflicting book', () => { 75 | return request(app) 76 | .post('/books') 77 | .send({name: 'testZAR', currency: 'USD'}) 78 | .expect(400) 79 | .then(res => { 80 | const err = res.body; 81 | assert.equal(err.success, false); 82 | assert.equal(err.errorCode, codes.MismatchedCurrency); 83 | }); 84 | }); 85 | }); 86 | 87 | describe('GET /books', () => { 88 | it('returns a list of books', () => { 89 | return request(app) 90 | .get('/books') 91 | .expect(200) 92 | .then(res => { 93 | const books = res.body; 94 | assert.ok(Array.isArray(books)); 95 | assert.equal(books.length, 2); 96 | assert.equal(books[0].name, 'testUSD'); 97 | assert.equal(books[1].name, 'testZAR'); 98 | }); 99 | }); 100 | }); 101 | 102 | describe('POST /books/{id}/ledger', () => { 103 | it('saves a valid entry', () => { 104 | return request(app) 105 | .post(`/books/${testUSD}/ledger`) 106 | .send({ 107 | memo: 'Buy Gold', 108 | timestamp: '2018-01-31 00:00:00Z', 109 | transactions: [ 110 | { 111 | account: 'Purchases:Local', 112 | debit: 2600, 113 | currency: 'USD' 114 | }, { 115 | account: 'GoldEFT:Foreign', 116 | credit: 26000, 117 | currency: 'ZAR', 118 | exchangeRate: 0.1 119 | } 120 | ] 121 | }) 122 | .expect(200) 123 | .then(res => { 124 | assert.ok(res.body.success); 125 | }); 126 | }); 127 | 128 | it('saves a valid entry on different book', () => { 129 | return request(app) 130 | .post(`/books/${testZAR}/ledger`) 131 | .send({ 132 | memo: 'Rent', 133 | timestamp: '2018-01-15 00:00:00Z', 134 | transactions: [ 135 | { 136 | account: 'Expenses:Rent', 137 | credit: 8600 138 | }, { 139 | account: 'Bank:Cash', 140 | debit: 8600 141 | } 142 | ] 143 | }) 144 | .expect(200) 145 | .then(res => { 146 | assert.ok(res.body.success); 147 | }); 148 | }); 149 | 150 | it('provides default currency if omitted', () => { 151 | return request(app) 152 | .post(`/books/${testUSD}/ledger`) 153 | .send({ 154 | memo: 'January Payroll', 155 | timestamp: '2018-01-15 00:00:00Z', 156 | transactions: [ 157 | { 158 | account: 'Payroll:Alice', 159 | credit: 5000 160 | }, 161 | { 162 | account: 'Payroll:Bob', 163 | credit: 5000 164 | }, 165 | { 166 | account: 'Expenses:Payroll', 167 | debit: 10000 168 | } 169 | ] 170 | }) 171 | .expect(200) 172 | .then(res => { 173 | assert.ok(res.body.success); 174 | }); 175 | }); 176 | 177 | it('auto-nets debits and credits', () => { 178 | return request(app) 179 | .post(`/books/${testUSD}/ledger`) 180 | .send({ 181 | memo: 'Buy Lambo', 182 | timestamp: '2018-01-16 00:00:00Z', 183 | transactions: [ 184 | { 185 | account: 'Assets:Cars', 186 | debit: 140000, 187 | credit: 20000, 188 | currency: 'USD' 189 | }, { 190 | account: 'Expenses:Cars', 191 | credit: 100000, 192 | currency: 'EUR', 193 | exchangeRate: 1.2 194 | } 195 | ] 196 | }) 197 | .expect(200) 198 | .then(res => { 199 | assert.ok(res.body.success); 200 | }); 201 | }); 202 | 203 | it('rejects if transactions don\'t balance', () => { 204 | return request(app) 205 | .post(`/books/${testUSD}/ledger`) 206 | .send({ 207 | memo: 'Buy Gold', 208 | timestamp: '2018-01-31 00:00:00Z', 209 | transactions: [ 210 | { 211 | account: 'Purchases:Local', 212 | debit: 2600, 213 | currency: 'USD' 214 | }, { 215 | account: 'GoldEFT:Foreign', 216 | credit: 25000, 217 | currency: 'ZAR', 218 | exchangeRate: 10.0 219 | } 220 | ] 221 | }) 222 | .expect(400) 223 | .then(res => { 224 | const err = res.body; 225 | assert.equal(err.success, false); 226 | assert.equal(err.errorCode, codes.EntryNotBalanced); 227 | }); 228 | }); 229 | }); 230 | 231 | describe('GET /books/{id}/ledger', () => { 232 | it('returns error for invalid book', () => { 233 | return request(app) 234 | .get('/books/999/ledger') 235 | .expect(400) 236 | .then(res => { 237 | const err = res.body; 238 | assert.equal(err.success, false); 239 | assert.equal(err.errorCode, codes.BookDoesNotExist); 240 | }); 241 | }); 242 | 243 | it('returns error for string as ID', () => { 244 | return request(app) 245 | .get('/books/invalid/ledger') 246 | .expect(400) 247 | .then(res => { 248 | const err = res.body; 249 | assert.equal(err.success, false); 250 | assert.equal(err.errorCode, codes.ValidationError); 251 | }); 252 | }); 253 | 254 | it('returns full ledger', () => { 255 | return request(app) 256 | .get(`/books/${testUSD}/ledger`) 257 | .expect(200) 258 | .then(res => { 259 | const ledger = res.body; 260 | assert.equal(ledger.book.id, testUSD); 261 | assert.ok(Array.isArray(ledger.entries)); 262 | assert.equal(ledger.entries.length, 3); 263 | assert.equal(ledger.entries[0].memo, 'January Payroll'); 264 | assert.equal(ledger.entries[0].transactions.length, 3); 265 | assert.equal(ledger.entries[1].memo, 'Buy Lambo'); 266 | }); 267 | }); 268 | 269 | it('returns first page of ledger', () => { 270 | return request(app) 271 | .get(`/books/${testUSD}/ledger`) 272 | .query({ perPage: 2}) 273 | .expect(200) 274 | .then(res => { 275 | const ledger = res.body; 276 | assert.equal(ledger.book.id, testUSD); 277 | assert.ok(Array.isArray(ledger.entries)); 278 | assert.equal(ledger.entries.length, 2); 279 | assert.equal(ledger.entries[0].memo, 'January Payroll'); 280 | assert.equal(ledger.entries[0].transactions.length, 3); 281 | assert.equal(ledger.entries[1].memo, 'Buy Lambo'); 282 | }); 283 | }); 284 | 285 | it('returns second page of ledger', () => { 286 | return request(app) 287 | .get(`/books/${testUSD}/ledger`) 288 | .query({ perPage: 2, page: 2 }) 289 | .expect(200) 290 | .then(res => { 291 | const ledger = res.body; 292 | assert.equal(ledger.book.id, testUSD); 293 | assert.ok(Array.isArray(ledger.entries)); 294 | assert.equal(ledger.entries.length, 1); 295 | assert.equal(ledger.entries[0].memo, 'Buy Gold'); 296 | assert.equal(ledger.entries[0].transactions.length, 2); 297 | }); 298 | }); 299 | 300 | it('returns entries with date constraints', () => { 301 | return request(app) 302 | .get(`/books/${testUSD}/ledger`) 303 | .query({ startDate: '2018-01-15', endDate: '2018-01-17' }) 304 | .expect(200) 305 | .then(res => { 306 | const ledger = res.body; 307 | assert.equal(ledger.book.id, testUSD); 308 | assert.ok(Array.isArray(ledger.entries)); 309 | assert.equal(ledger.entries.length, 2); 310 | assert.equal(ledger.entries[0].memo, 'January Payroll'); 311 | }); 312 | }); 313 | }); 314 | 315 | describe('GET /books/{id}/accounts', () => { 316 | it('returns an array of strings', () => { 317 | return request(app) 318 | .get(`/books/${testUSD}/accounts`) 319 | .expect(200) 320 | .then(res => { 321 | const accounts = res.body; 322 | assert.equal(accounts.length, 12); 323 | }); 324 | }); 325 | }); 326 | 327 | describe('GET /books/{id}/transactions', () => { 328 | it('returns transactions for given account', () => { 329 | return request(app) 330 | .get(`/books/${testUSD}/transactions`) 331 | .query({ accounts: 'Payroll'}) 332 | .expect(200) 333 | .then(res => { 334 | const txs = res.body; 335 | assert.equal(txs.length, 2); 336 | assert.equal(txs[0].account, 'Payroll:Alice'); 337 | assert.equal(txs[0].credit, '5000'); 338 | assert.equal(txs[1].account, 'Payroll:Bob'); 339 | assert.equal(txs[1].credit, '5000'); 340 | }); 341 | }); 342 | }); 343 | 344 | describe('GET /books/{id}/balance', () => { 345 | it('returns the balance for an account', () => { 346 | return request(app) 347 | .get(`/books/${testUSD}/balance`) 348 | .query({ account: 'Expenses'}) 349 | .expect(200) 350 | .then(res => { 351 | const bal = res.body; 352 | assert.equal(bal.creditTotal, 120000); 353 | assert.equal(bal.debitTotal, 10000); 354 | assert.equal(bal.balance, 110000); 355 | assert.equal(bal.currency, 'USD'); 356 | assert.equal(bal.numTransactions, 2); 357 | }); 358 | }); 359 | 360 | it('returns the balance in base currency', () => { 361 | return request(app) 362 | .get(`/books/${testUSD}/balance`) 363 | .query({ account: 'GoldEFT:Foreign', inQuoteCurrency: false}) 364 | .expect(200) 365 | .then(res => { 366 | const bal = res.body; 367 | assert.equal(bal.creditTotal, 26000); 368 | assert.equal(bal.debitTotal, 0); 369 | assert.equal(bal.balance, 26000); 370 | assert.equal(bal.currency, 'ZAR'); 371 | assert.equal(bal.numTransactions, 1); 372 | }); 373 | }); 374 | 375 | it('returns the unrealised profit', () => { 376 | return request(app) 377 | .post(`/books/${testUSD}/marktomarket`) 378 | .send({ accounts: ['GoldEFT:Foreign', 'Purchases:Local'], exchangeRates: { USD: 1, ZAR: 20 }}) 379 | .expect(200) 380 | .then(res => { 381 | const bal = res.body; 382 | assert.deepEqual(bal, { 383 | 'GoldEFT:Foreign': 1300, 384 | 'Purchases:Local': -2600, 385 | unrealizedProfit: -1300 386 | }); 387 | }); 388 | }); 389 | 390 | it('returns an error if exchange rates are missing', () => { 391 | return request(app) 392 | .post(`/books/${testUSD}/marktomarket`) 393 | .send({ accounts: ['GoldEFT:Foreign', 'Purchases:Local'], exchangeRates: { ZAR: 20 }}) 394 | .expect(400) 395 | .then(res => { 396 | assert.equal(res.body.success, false); 397 | assert.equal(res.body.errorCode, codes.MissingInput); 398 | }); 399 | }); 400 | }); 401 | }); 402 | --------------------------------------------------------------------------------