├── .env-template ├── tslint.json ├── .travis.yml ├── cli.js ├── .gitignore ├── tsconfig.json ├── .npmignore ├── src ├── cli │ ├── cli.ts │ ├── cli-user.ts │ ├── cli-account.ts │ ├── cli-statement.ts │ ├── cli-transaction.ts │ ├── cli-transfer.ts │ └── cli-standing-order.ts └── kontist-client.ts ├── package.json ├── README.md └── test └── unit └── kontist-client.spec.ts /.env-template: -------------------------------------------------------------------------------- 1 | KONTIST_USER=user@example.com 2 | KONTIST_PASSWORD=yourpassword -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest", 3 | "rules": { 4 | // required for chai 5 | "no-unused-expression": false 6 | } 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | before_install: 5 | - npm install -g typescript 6 | - npm install 7 | - tsc 8 | script: 9 | - npm test 10 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const dirname = path.dirname; 6 | 7 | // redirect commands to dist/cli subfolder 8 | process.argv[1] = dirname(process.argv[1], '.js') + "/dist/src/cli/cli.js" 9 | 10 | require("./dist/src/cli/cli"); 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore npm files 2 | node_modules 3 | npm-debug.log 4 | 5 | # ignore Mac files 6 | .DS_Store 7 | 8 | # IDE settings 9 | .idea 10 | .vscode/* 11 | !.vscode/launch.json 12 | 13 | # Local settings 14 | .env 15 | 16 | # Local exports 17 | *.pdf 18 | *.qif 19 | .transfer-tmp.json 20 | 21 | # ignore generated code 22 | dist/* 23 | coverage/* 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "outDir": "./dist", 5 | "declaration":true, 6 | "target": "es6", 7 | "sourceMap": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true 10 | }, 11 | "compileOnSave": false, 12 | "filesGlob": [ 13 | "!node_modules/**" 14 | ] 15 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ignore npm files 2 | node_modules 3 | npm-debug.log 4 | 5 | # ignore Mac files 6 | .DS_Store 7 | 8 | # IDE settings 9 | .idea 10 | .vscode/* 11 | !.vscode/launch.json 12 | 13 | # Local settings 14 | .env 15 | 16 | # Local exports 17 | *.pdf 18 | *.qif 19 | .transfer-tmp.json 20 | 21 | # ignore source 22 | src/* 23 | tsconfig.json 24 | tslint.json 25 | 26 | # ignore tests 27 | test/* 28 | coverage/* 29 | dist/test/* -------------------------------------------------------------------------------- /src/cli/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Command } from "commander"; 4 | 5 | const program = new Command(); 6 | 7 | program 8 | .version("0.4.5") 9 | .command("transaction ", "list transactions, format can be json or qif") 10 | .command("standing-order ", "manage standing orders") 11 | .command("account ", "list accounts") 12 | .command("user ", "return information about current user") 13 | .command("statement ", "export pdf for e.g. 2017 02") 14 | .command("transfer ", "list, init or confirm transfers"); 15 | 16 | program.parse(process.argv); 17 | if (!process.argv.slice(2).length) { 18 | program.outputHelp(); 19 | } 20 | 21 | module.exports = {}; 22 | -------------------------------------------------------------------------------- /src/cli/cli-user.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import { Command } from "commander"; 8 | 9 | const program = new Command(); 10 | 11 | program 12 | .command("info") 13 | .description("return all user information") 14 | .action(async () => { 15 | try { 16 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 17 | const user = await kontist.getUser(); 18 | process.stdout.write(JSON.stringify(user, null, 4)); 19 | } catch (error) { 20 | // tslint:disable-next-line:no-console 21 | console.error(error); 22 | } 23 | }); 24 | 25 | program.parse(process.argv); 26 | if (!process.argv.slice(2).length) { 27 | program.outputHelp(); 28 | } 29 | -------------------------------------------------------------------------------- /src/cli/cli-account.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import { Command } from "commander"; 8 | 9 | const program = new Command(); 10 | 11 | program 12 | .command("list") 13 | .description("list all accounts for the current user") 14 | .action(async () => { 15 | try { 16 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 17 | const accounts = await kontist.getAccounts(); 18 | process.stdout.write(JSON.stringify(accounts, null, 4)); 19 | } catch (error) { 20 | // tslint:disable-next-line:no-console 21 | console.error(error); 22 | } 23 | }); 24 | 25 | program.parse(process.argv); 26 | if (!process.argv.slice(2).length) { 27 | program.outputHelp(); 28 | } 29 | -------------------------------------------------------------------------------- /src/cli/cli-statement.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import { Command } from "commander"; 8 | 9 | const program = new Command(); 10 | 11 | program 12 | .command("export ") 13 | .description("download a PDF, e.g. export 2017 02") 14 | .action(async (year: string, month: string) => { 15 | try { 16 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 17 | const statement = await kontist.getStatement(year, month); 18 | process.stdout.write(statement); 19 | } catch (error) { 20 | // tslint:disable-next-line:no-console 21 | console.error(error); 22 | } 23 | }); 24 | 25 | program.parse(process.argv); 26 | if (!process.argv.slice(2).length) { 27 | program.outputHelp(); 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@netnexus/ikontist", 3 | "version": "0.4.15", 4 | "description": "Connect to Kontist, fetch and create and transfers, export transactions as QIF", 5 | "main": "dist/src/kontist-client.js", 6 | "types": "dist/src/kontist-client.d.ts", 7 | "author": "", 8 | "license": "ISC", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/netnexus/IKontist.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/netnexus/IKontist/issues" 15 | }, 16 | "homepage": "https://github.com/netnexus/IKontist#readme", 17 | "scripts": { 18 | "test": "./node_modules/.bin/istanbul -x='cli.js' --include-all-sources cover ./node_modules/.bin/_mocha $(find dist/test/unit -name '*.js') --dir coverage/unit --print none && ./node_modules/.bin/remap-istanbul -i coverage/unit/coverage.json -o coverage/unit/coverage-ts -t html" 19 | }, 20 | "dependencies": { 21 | "@types/commander": "~2.12.2", 22 | "@types/dotenv": "^6.1.1", 23 | "async-file": "~2.0.2", 24 | "axios": "^0.21.1", 25 | "commander": "^2.20.3", 26 | "dotenv": "^8.2.0", 27 | "lodash": "^4.17.20", 28 | "qif-writer": "~0.1.0" 29 | }, 30 | "peerDependencies": { 31 | "osa-imessage": "~2.4.2" 32 | }, 33 | "devDependencies": { 34 | "@types/chai": "^4.2.14", 35 | "@types/lodash": "^4.14.165", 36 | "@types/mocha": "^5.2.7", 37 | "@types/node": "^12.19.8", 38 | "@types/sinon": "^7.5.2", 39 | "chai": "^4.2.0", 40 | "istanbul": "~0.4.5", 41 | "mocha": "^6.2.3", 42 | "remap-istanbul": "^0.13.0", 43 | "sinon": "^7.5.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/cli/cli-transaction.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import { Command } from "commander"; 8 | 9 | const program = new Command(); 10 | const FORMAT_JSON = "json"; 11 | const FORMAT_QIF = "qif"; 12 | 13 | program 14 | .command("list [format] [accountId]") 15 | .description("format can be json or qif") 16 | .action(async (format = FORMAT_JSON, accountId?: number) => { 17 | try { 18 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 19 | accountId = accountId || (await kontist.getAccounts())[0].id; 20 | const transactions = await kontist.getTransactions(accountId); 21 | switch (format) { 22 | case FORMAT_JSON: { 23 | process.stdout.write(JSON.stringify(transactions, null, 4)); 24 | break; 25 | } 26 | case FORMAT_QIF: { 27 | const qif = require("qif-writer"); 28 | const data = transactions.map((row) => ({ 29 | amount: row.amount / 100 * (row.from === accountId ? -1 : 1), 30 | date: new Date(row.bookingDate).toLocaleDateString("en-US"), 31 | memo: row.purpose, 32 | payee: row.name, 33 | })); 34 | qif.write(data, { type: "Bank" }); 35 | break; 36 | } 37 | default: 38 | throw new Error("Unknown format " + format); 39 | } 40 | } catch (error) { 41 | // tslint:disable-next-line:no-console 42 | console.error(error); 43 | } 44 | }); 45 | 46 | program.parse(process.argv); 47 | if (!process.argv.slice(2).length) { 48 | program.outputHelp(); 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated - please switch to the official SDK 2 | *This is deprecated as Kontist is now providing an official SDK, please see https://kontist.dev and https://github.com/kontist/sdk* 3 | 4 | # IKontist 5 | This repository is not an official repository of the Kontist GmbH. 6 | It connects to a currently undocumented REST API. 7 | 8 | [![Build Status](https://travis-ci.org/netnexus/IKontist.svg?branch=master)](https://travis-ci.org/netnexus/IKontist) 9 | 10 | ## What does it do? 11 | We provide 12 | 1. a command line tool and 13 | 2. a JavaScript API. 14 | 15 | Currently it can fetch transactions, transfers, statements and can create and confirm new transfers. 16 | 17 | Please note that this project is in a early stage and support is welcome. 18 | 19 | ## How to use the CLI 20 | ### Prerequisites 21 | * [node](https://nodejs.org) and npm installed 22 | * Install with `npm install @netnexus/ikontist` 23 | * Add your Kontist username and password either to ENVs called `KONTIST_USER` and `KONTIST_PASSWORD` or a .env file (just rename `.env-template` to `.env` and replace the credentials). 24 | 25 | ### Examples 26 | ```bash 27 | node cli.js transaction list json 28 | ``` 29 | 30 | will return 31 | ```json 32 | [ 33 | { 34 | "amount": 100, 35 | "bookingDate": "2016-12-16T00:00:00.000Z", 36 | "bookingType": "SEPA_CREDIT_TRANSFER", 37 | "category": null, 38 | "e2eId": "NOTPROVIDED", 39 | "foreignCurrency": null, 40 | "from": null, 41 | "iban": "DEXXXXX", 42 | "id": 4711, 43 | "name": "Foo, Bar", 44 | "originalAmount": null, 45 | "pendingFrom": null, 46 | "purpose": "Fancy Friday, Baby", 47 | "to": 3214, 48 | "type": null, 49 | "valutaDate": "2016-12-16T00:00:00.000Z", 50 | "paymentMethod": "bank_account" 51 | } 52 | ] 53 | ``` 54 | 55 | ```bash 56 | node cli.js transaction list qif 57 | ``` 58 | 59 | will return 60 | ``` 61 | !Type:Bank 62 | D8/2/2017 63 | T8199 64 | PExample GmbH 65 | MRNr. ABC 66 | ^ 67 | D8/2/2017 68 | T2142 69 | PExample 70 | Mdescription 71 | ^ 72 | ``` 73 | 74 | So an easy way to create a qif export would be: 75 | ```bash 76 | node cli.js transaction list qif > my-account.qif 77 | ``` 78 | 79 | To start a transfer you can use `transfer init` and `transfer confirm`: 80 | ```bash 81 | # init transfer of 1€ to John Doe 82 | node cli.js transfer init "John Doe" DE89370400440532013000 100 "test description" 83 | 84 | # wait for sms with token (e.g. 252899) 85 | node cli.js transfer confirm 252899 86 | ``` 87 | 88 | On macOS you can even use the `--auto` option with `init` to poll iMessages for the tan and automatically confirm the transfer: 89 | 90 | ```bash 91 | # install peer dependency 92 | npm i osa-imessage 93 | 94 | # init and auto confirm transfer of 1€ to John Doe 95 | node cli.js transfer init "John Doe" DE89370400440532013000 100 "test description" --auto 96 | ``` 97 | 98 | See more commands with 99 | 100 | ```bash 101 | node cli.js --help 102 | ``` 103 | 104 | ## How to use the API 105 | 106 | After instantiation of the class you need to login with your Kontist username and password, e.g. 107 | 108 | ```ts 109 | const ikontist = require("kontist-client"); 110 | const client = new ikontist.KontistClient(); 111 | client.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD).then(function() { 112 | // do further calls to kontist here 113 | }) 114 | ``` 115 | 116 | Of course you can use import instead of require: 117 | 118 | ```ts 119 | import { KontistClient } from "@netnexus/ikontist"; 120 | const client = new KontistClient(); 121 | ``` 122 | 123 | Please have a look at the `kontist-client.js`. Currently it provides methods for the following endpoints: 124 | ```ts 125 | client.login(email, password) 126 | client.getUser() 127 | client.getAccounts() 128 | client.getTransactions(accountId, limit) 129 | client.getFutureTransactions(accountId, limit) 130 | client.getTransfers(accountId, limit) 131 | client.initiateTransfer(accountId, recipient, iban, amount, note) 132 | client.confirmTransfer(accountId, transferId, authorizationToken, recipient, iban, amount, note) 133 | client.getStatement(accountId, year, month) 134 | client.initiateStandingOrder(...) 135 | client.confirmStandingOrder(...) 136 | client.initCancelStandingOrder(...) 137 | client.getWireTransferSuggestions(...) 138 | ``` 139 | 140 | -------------------------------------------------------------------------------- /src/cli/cli-transfer.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import * as fs from "async-file"; 8 | import { Command } from "commander"; 9 | 10 | const tmpFile = ".transfer-tmp.json"; 11 | const program = new Command(); 12 | 13 | program 14 | .command("list [accountId]") 15 | .description("list all transfers") 16 | .action(async (accountId?: number) => { 17 | try { 18 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 19 | accountId = accountId || (await kontist.getAccounts())[0].id; 20 | const transactions = kontist.getTransfers(accountId); 21 | process.stdout.write(JSON.stringify(transactions, null, 4)); 22 | } catch (error) { 23 | // tslint:disable-next-line:no-console 24 | console.error(error); 25 | } 26 | }); 27 | 28 | program 29 | .command("init [accountId]") 30 | .description("initiate a transfer, amount is EUR in cents") 31 | .option("-a, --auto", "Read pin from imessages and auto confirm (requires macOS)") 32 | .action(async (recipient, iban, amount, note, accountId: number, options: any) => { 33 | try { 34 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 35 | accountId = accountId || (await kontist.getAccounts())[0].id; 36 | let requested = false; 37 | if (options.auto) { 38 | try { 39 | const imessage = require("osa-imessage"); 40 | imessage.listen().on("message", async (msg) => { 41 | if (requested && msg.handle === "solarisbank" && msg.text.match(iban) !== null) { 42 | const token = msg.text.match(/: (.*)\./)[1]; 43 | const transferId = result.links.self.split("/").slice(-1); 44 | const confirmResult = await kontist.confirmTransfer(accountId, 45 | transferId, token, recipient, iban, +amount, note); 46 | process.stdout.write(JSON.stringify(confirmResult, null, 4)); 47 | process.exit(); 48 | } 49 | }); 50 | } catch (e) { 51 | // tslint:disable-next-line:no-console 52 | console.error("'--auto' only works on a mac and with osa-imessage installed (npm i osa-imessage)"); 53 | return; 54 | } 55 | } 56 | 57 | const result = await kontist.initiateTransfer(accountId, recipient, iban, +amount, note); 58 | requested = true; // we only want to look at the new incoming iMessages. 59 | process.stdout.write(JSON.stringify(result, null, 4)); 60 | 61 | if (!options.auto) { 62 | // save tmp file for confirm 63 | await fs.writeFile(tmpFile, JSON.stringify({ ...result, accountId }), "utf8"); 64 | } 65 | } catch (error) { 66 | // tslint:disable-next-line:no-console 67 | console.error(error); 68 | } 69 | }); 70 | 71 | program 72 | .command("confirm [transferId] [recipient] [iban] [amount] [note] [accountId]") 73 | .description("confirm a transfer, use values from previous init call or or give explicitly") 74 | .action(async (token, transferId, recipient, iban, amount, note, accountId) => { 75 | try { 76 | // restore data from tmp file 77 | try { 78 | const data = await fs.readFile(tmpFile, "utf8"); 79 | const json = JSON.parse(data); 80 | transferId = json.links.self.split("/").slice(-1); 81 | recipient = recipient || json.recipient; 82 | iban = iban || json.iban; 83 | amount = amount || json.amount; 84 | note = note || json.note; 85 | accountId = accountId || json.accountId; 86 | } catch (e) { 87 | // tslint:disable-next-line:no-console 88 | console.error(e); 89 | } 90 | 91 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 92 | accountId = accountId || (await kontist.getAccounts())[0].id; 93 | const result = await kontist.confirmTransfer(accountId, transferId, token, recipient, iban, +amount, note); 94 | process.stdout.write(JSON.stringify(result, null, 4)); 95 | await fs.unlink(tmpFile); 96 | } catch (error) { 97 | // tslint:disable-next-line:no-console 98 | console.error(error); 99 | } 100 | }); 101 | 102 | program.parse(process.argv); 103 | if (!process.argv.slice(2).length) { 104 | program.outputHelp(); 105 | } 106 | -------------------------------------------------------------------------------- /src/cli/cli-standing-order.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as dotenv from "dotenv"; 4 | dotenv.config(); 5 | import { KontistClient } from "../kontist-client"; 6 | const kontist = new KontistClient(); 7 | import * as fs from "async-file"; 8 | import { Command } from "commander"; 9 | 10 | const tmpFile = ".standing-order-tmp.json"; 11 | const program = new Command(); 12 | 13 | program 14 | .command("list [format] [accountId]") 15 | .description("list standing orders") 16 | .action(async (accountId?: number) => { 17 | try { 18 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 19 | accountId = accountId || (await kontist.getAccounts())[0].id; 20 | const standingOrders = await kontist.getStandingOrders(accountId); 21 | process.stdout.write(JSON.stringify(standingOrders, null, 4)); 22 | } catch (error) { 23 | // tslint:disable-next-line:no-console 24 | console.error(error); 25 | } 26 | }); 27 | 28 | program 29 | .command("init [accountId]") 30 | .description("initiate a standing order, amount is EUR in cents") 31 | .option("-a, --auto", "Read pin from imessages and auto confirm (requires macOS)") 32 | .action(async (recipient, iban, amount, note, reoccurrence, start, accountId: number, options: any) => { 33 | try { 34 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 35 | accountId = accountId || (await kontist.getAccounts())[0].id; 36 | let requested = false; 37 | if (options.auto) { 38 | try { 39 | const imessage = require("osa-imessage"); 40 | imessage.listen().on("message", async (msg) => { 41 | if (requested && msg.handle === "solarisbank" && msg.text.match(iban) !== null) { 42 | const token = msg.text.match(/: (.*)\./)[1]; 43 | const requestId = result.requestId; 44 | const confirmResult = await kontist.confirmStandingOrder(accountId, requestId, token); 45 | process.stdout.write(JSON.stringify(confirmResult, null, 4)); 46 | process.exit(); 47 | } 48 | }); 49 | } catch (e) { 50 | // tslint:disable-next-line:no-console 51 | console.error("'--auto' only works on a mac and with osa-imessage installed (npm i osa-imessage)"); 52 | return; 53 | } 54 | } 55 | 56 | const result = await kontist 57 | .initiateStandingOrder(accountId, recipient, iban, +amount, note, reoccurrence, start); 58 | requested = true; // we only want to look at the new incoming iMessages. 59 | process.stdout.write(JSON.stringify(result, null, 4)); 60 | 61 | if (!options.auto) { 62 | // save tmp file for confirm 63 | await fs.writeFile(tmpFile, JSON.stringify({ ...result, accountId }), "utf8"); 64 | } 65 | } catch (error) { 66 | // tslint:disable-next-line:no-console 67 | console.error(error); 68 | } 69 | }); 70 | 71 | program 72 | .command("confirm [requestId] [accountId]") 73 | .description("confirm a standing order, use values from previous init call or or give explicitly") 74 | .action(async (token, requestId, accountId) => { 75 | try { 76 | // restore data from tmp file 77 | try { 78 | const data = await fs.readFile(tmpFile, "utf8"); 79 | const json = JSON.parse(data); 80 | requestId = requestId || json.requestId; 81 | accountId = accountId || json.accountId; 82 | } catch (e) { 83 | // tslint:disable-next-line:no-console 84 | console.error(e); 85 | } 86 | 87 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 88 | accountId = accountId || (await kontist.getAccounts())[0].id; 89 | const result = await kontist.confirmStandingOrder(accountId, requestId, token); 90 | process.stdout.write(JSON.stringify(result, null, 4)); 91 | await fs.unlink(tmpFile); 92 | } catch (error) { 93 | // tslint:disable-next-line:no-console 94 | console.error(error); 95 | } 96 | }); 97 | 98 | program 99 | .command("cancel [accountId]") 100 | .description("cancel a standing order") 101 | .option("-a, --auto", "Read pin from imessages and auto confirm (requires macOS)") 102 | .action(async (standingOrderId, accountId: number, options: any) => { 103 | try { 104 | await kontist.login(process.env.KONTIST_USER, process.env.KONTIST_PASSWORD); 105 | accountId = accountId || (await kontist.getAccounts())[0].id; 106 | let requested = false; 107 | if (options.auto) { 108 | try { 109 | const imessage = require("osa-imessage"); 110 | imessage.listen().on("message", async (msg) => { 111 | if (requested && msg.handle === "solarisbank") { 112 | const token = msg.text.match(/: (.*)\./)[1]; 113 | const requestId = result.requestId; 114 | const confirmResult = await kontist.confirmStandingOrder(accountId, requestId, token); 115 | process.stdout.write(JSON.stringify(confirmResult, null, 4)); 116 | process.exit(); 117 | } 118 | }); 119 | } catch (e) { 120 | // tslint:disable-next-line:no-console 121 | console.error("'--auto' only works on a mac and with osa-imessage installed (npm i osa-imessage)"); 122 | return; 123 | } 124 | } 125 | 126 | const result = await kontist.initCancelStandingOrder(accountId, standingOrderId); 127 | requested = true; // we only want to look at the new incoming iMessages. 128 | process.stdout.write(JSON.stringify(result, null, 4)); 129 | 130 | if (!options.auto) { 131 | // save tmp file for confirm 132 | await fs.writeFile(tmpFile, JSON.stringify({ ...result, accountId }), "utf8"); 133 | } 134 | } catch (error) { 135 | // tslint:disable-next-line:no-console 136 | console.error(error); 137 | } 138 | }); 139 | 140 | program.parse(process.argv); 141 | if (!process.argv.slice(2).length) { 142 | program.outputHelp(); 143 | } 144 | -------------------------------------------------------------------------------- /src/kontist-client.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "lodash"; 2 | 3 | import { AxiosResponse, Method, default as axiosClient } from "axios"; 4 | 5 | const BASE_URL = "https://api.kontist.com"; 6 | 7 | export class KontistClient { 8 | private token: string; 9 | 10 | public constructor(private axios = axiosClient.create()) { 11 | _.set(this, "axios.defaults.headers.common", {}); 12 | } 13 | 14 | /** 15 | * Return information for logged in user. (via constructor {user: "", password: ""} parameters) 16 | */ 17 | public getUser(): Promise { 18 | return this.request("/api/user"); 19 | } 20 | 21 | /** 22 | * Return list of accounts. 23 | */ 24 | public getAccounts(): Promise { 25 | return this.request("/api/accounts"); 26 | } 27 | 28 | /** 29 | * Return list of transactions. 30 | * @param {number} accountId 31 | * @param {number} limit 32 | */ 33 | public async getTransactions(accountId: number, limit = Number.MAX_SAFE_INTEGER): Promise { 34 | return this.fetchAmount(`/api/accounts/${accountId}/transactions`, limit); 35 | } 36 | 37 | /** 38 | * Return list of future transactions. 39 | * @param {number} accountId 40 | * @param {number} limit 41 | */ 42 | public async getFutureTransactions(accountId: number, limit = Number.MAX_SAFE_INTEGER): Promise { 43 | return this.fetchAmount(`/api/accounts/${accountId}/future-transactions`, limit); 44 | } 45 | 46 | /** 47 | * Return list of standing orders. 48 | * @param {number} accountId 49 | * @param {number} limit 50 | */ 51 | public async getStandingOrders(accountId: number): Promise { 52 | return this.request(`/api/accounts/${accountId}/standing-orders`); 53 | } 54 | 55 | /** 56 | * Create a new standing order, needs to be confirmed with the ´confirmStandingOrder` method and a 57 | * authorizationToken (you will receive via sms). 58 | * @param {number} accountId 59 | * @param {string} recipient 60 | * @param {string} iban 61 | * @param {number} amount cents 62 | * @param {string} note 63 | * @param {string} reoccurrence "MONTHLY" | "QUARTERLY" | "EVERY_SIX_MONTHS" | "ANNUALLY" 64 | * @param {string} firstExecutionDate e.g. "2018-08-29T00:00:00+00:00" 65 | */ 66 | public initiateStandingOrder( 67 | accountId: number, 68 | recipient: string, 69 | iban: string, 70 | amount: number, 71 | note: string, 72 | reoccurrence: "MONTHLY" | "QUARTERLY" | "EVERY_SIX_MONTHS" | "ANNUALLY", 73 | firstExecutionDate: string, 74 | ): Promise { 75 | return this.request(`/api/accounts/${accountId}/standing-orders`, "post", { 76 | amount, 77 | e2eId: null, 78 | firstExecutionDate, 79 | iban, 80 | note, 81 | recipient, 82 | reoccurrence, 83 | standingOrderToggle: true, 84 | }); 85 | } 86 | 87 | /** 88 | * Confirm a standing order or confirm cancelation. 89 | * @param {number} accountId 90 | * @param {string} requestId 91 | * @param {string} authorizationToken 92 | */ 93 | public confirmStandingOrder( 94 | accountId: number, 95 | requestId: string, 96 | authorizationToken: string, 97 | ): Promise { 98 | return this.request(`/api/accounts/${accountId}/standing-orders/confirm`, 99 | "post", { 100 | authorizationToken, 101 | requestId, 102 | }); 103 | } 104 | 105 | /** 106 | * Cancel a standing order. 107 | * @param {number} accountId 108 | * @param {string} standingOrderId 109 | */ 110 | public initCancelStandingOrder( 111 | accountId: number, 112 | standingOrderId: string, 113 | ): Promise { 114 | return this.request(`/api/accounts/${accountId}/standing-orders/${standingOrderId}/cancel`, "patch", {}); 115 | } 116 | 117 | /** 118 | * Return list of wire transfer suggestions. 119 | * @param {string} query part of name or iban 120 | */ 121 | public async getWireTransferSuggestions(query: string): Promise { 122 | query = query.toLowerCase(); 123 | const suggestions = await this.request(`/api/wire-transfer-suggestions`); 124 | return suggestions.filter((sug) => ( 125 | sug.name.toLowerCase().indexOf(query) > -1 || sug.iban.toLowerCase().indexOf(query) > -1)); 126 | } 127 | 128 | /** 129 | * Return list of transfers. 130 | * @param {number} accountId 131 | * @param {number} limit 132 | */ 133 | public getTransfers(accountId: number, limit = Number.MAX_SAFE_INTEGER): Promise { 134 | return this.fetchAmount(`/api/accounts/${accountId}/transfer`, limit); 135 | } 136 | 137 | /** 138 | * Create a new transfer, needs to be confirmed with the ´confirmTransfer` method and a 139 | * authorizationToken (you will receive via sms). 140 | * @param {number} accountId 141 | * @param {string} recipient 142 | * @param {string} iban 143 | * @param {number} amount cents 144 | * @param {string} note 145 | */ 146 | public initiateTransfer( 147 | accountId: number, 148 | recipient: string, 149 | iban: string, 150 | amount: number, 151 | note: string, 152 | ): Promise { 153 | return this.request(`/api/accounts/${accountId}/transfer`, "post", { recipient, iban, amount, note }); 154 | } 155 | 156 | /** 157 | * Confirm a transfer with the same parameters as used in `initiateTransfer` and additionally 158 | * the id returned by the latter and a token provided usually via sms. 159 | * @param {number} accountId 160 | * @param {string} transferId 161 | * @param {string} authorizationToken 162 | * @param {string} recipient 163 | * @param {string} iban 164 | * @param {number} amount cents 165 | * @param {string} note 166 | */ 167 | public confirmTransfer( 168 | accountId: number, 169 | transferId: string, 170 | authorizationToken: string, 171 | recipient: string, 172 | iban: string, 173 | amount: number, 174 | note: string, 175 | ): Promise { 176 | return this.request(`/api/accounts/${accountId}/transfer/${transferId}`, 177 | "put", { authorizationToken, recipient, iban, amount, note }); 178 | } 179 | 180 | /** 181 | * Return a pdf statement. 182 | */ 183 | public getStatement(year: string, month: string): Promise { 184 | return this.request(`/api/user/statements/${year}/${month}`); 185 | } 186 | 187 | /** 188 | * Return a promise with a jwt token. 189 | * 190 | * @param email 191 | * @param password 192 | */ 193 | public async login(email: string, password: string): Promise { 194 | this.token = null; 195 | const result = await this.request("/api/user/auth-token", "post", { email, password }); 196 | this.token = result.token; 197 | return this.token; 198 | } 199 | 200 | /** 201 | * Fetch (unlimited) number of results. 202 | * 203 | * @param startUrl 204 | * @param limit 205 | */ 206 | private async fetchAmount(startUrl: string, limit: number) { 207 | let total = limit; 208 | let next = startUrl; 209 | const results = []; 210 | while (results.length < Math.min(total, limit)) { 211 | const data = await this.request(next); 212 | total = data.total; 213 | next = data.next; 214 | results.push(...data.results); 215 | } 216 | return results.slice(0, limit); 217 | } 218 | 219 | /** 220 | * Helper to create promise with call to an endpoint. 221 | * 222 | * @param {string} endpoint 223 | * @param {string} method = get 224 | * @param {*} data 225 | */ 226 | private async request(endpoint: string, method: Method = "get", data?: any): Promise { 227 | const headers: any = { 228 | "Content-Type": "application/json", 229 | "accept": "application/vnd.kontist.transactionlist.v2.1+json", 230 | }; 231 | if (this.token) { 232 | headers.Authorization = "Bearer " + this.token; 233 | } 234 | return new Promise((resolve, reject) => { 235 | this.axios({ 236 | data, 237 | headers, 238 | maxRedirects: 0, 239 | method, 240 | url: BASE_URL + endpoint, 241 | }) 242 | .then((result) => this.handleResponse(result, resolve, reject)) 243 | .catch((err) => reject(err)); 244 | }); 245 | } 246 | 247 | /** 248 | * Calls rejecter when response has invalid status, calls resolver(data) if status is valid. 249 | * 250 | * @param {*} data 251 | * @param {*} response 252 | * @param {*} resolver 253 | * @param {*} rejecter 254 | */ 255 | private async handleResponse( 256 | result: AxiosResponse, 257 | resolver: (data: any) => void, 258 | rejecter: (error: Error) => void, 259 | ): Promise { 260 | if (result.status === 302) { 261 | // manually redirect (w/o headers) to avoid problems an S3 when multiple Auth params are send. 262 | // tslint:disable-next-line:no-string-literal 263 | return this.axios({ method: "get", url: result.headers.location }) 264 | .then((redirectedResult) => this.handleResponse(redirectedResult, resolver, rejecter)) 265 | .catch((err) => rejecter(err)); 266 | } 267 | if (result.status < 200 || result.status > 299) { 268 | // tslint:disable-next-line:no-console 269 | console.debug(result); 270 | rejecter(new Error(result.statusText)); 271 | } 272 | resolver(result.data); 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /test/unit/kontist-client.spec.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | 3 | import * as sinon from "sinon"; 4 | 5 | import { KontistClient } from "../../src/kontist-client"; 6 | import { expect } from "chai"; 7 | 8 | describe("KontistClient", () => { 9 | let client: KontistClient; 10 | let sandbox: sinon.SinonSandbox; 11 | let axiosStub: any; 12 | 13 | afterEach(() => { 14 | sandbox.restore(); 15 | }); 16 | beforeEach(() => { 17 | sandbox = sinon.createSandbox(); 18 | axiosStub = sandbox.stub().returns(Promise.resolve({ 19 | data: {}, 20 | status: 200, 21 | })); 22 | client = new KontistClient(axiosStub); 23 | client = new KontistClient(axiosStub); 24 | }); 25 | 26 | describe("#getUser()", () => { 27 | it("should call correct endpoint", async () => { 28 | // arrange 29 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 30 | 31 | // act 32 | await client.getUser(); 33 | 34 | // assert 35 | sinon.assert.calledWith(spyOnRequest, "/api/user"); 36 | }); 37 | }); 38 | 39 | describe("#getAccounts()", () => { 40 | it("should call correct endpoint", async () => { 41 | // arrange 42 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 43 | 44 | // act 45 | await client.getAccounts(); 46 | 47 | // assert 48 | sinon.assert.calledWith(spyOnRequest, "/api/accounts"); 49 | }); 50 | }); 51 | 52 | describe("#getTransactions()", () => { 53 | it("should call correct endpoint", async () => { 54 | // arrange 55 | axiosStub = sandbox.stub() 56 | .onFirstCall().resolves({ 57 | data: { 58 | next: "/api/accounts/1/transactions?page=2", 59 | results: [{ 60 | amount: 123, 61 | id: 1, 62 | }], 63 | total: 2, 64 | }, 65 | status: 200, 66 | }) 67 | .onSecondCall().resolves({ 68 | data: { 69 | next: null, 70 | results: [{ 71 | amount: 345, 72 | id: 2, 73 | }], 74 | total: 2, 75 | }, 76 | status: 200, 77 | }); 78 | client = new KontistClient(axiosStub); 79 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 80 | 81 | // act 82 | const transactions = await client.getTransactions(1); 83 | 84 | // assert 85 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/transactions"); 86 | expect(transactions).to.eql([{ amount: 123, id: 1 }, { amount: 345, id: 2 }]); 87 | }); 88 | it("should limit result", async () => { 89 | // arrange 90 | axiosStub = sandbox.stub() 91 | 92 | .onFirstCall().resolves({ 93 | data: { 94 | next: "/api/accounts/1/transactions?page=2", 95 | results: [{ 96 | amount: 123, 97 | }], 98 | total: 2, 99 | }, 100 | status: 200, 101 | }) 102 | .onSecondCall().resolves({ 103 | data: { 104 | next: null, 105 | results: [{ 106 | amount: 345, 107 | }], 108 | total: 2, 109 | }, 110 | status: 200, 111 | }); 112 | client = new KontistClient(axiosStub); 113 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 114 | 115 | // act 116 | const transactions = await client.getTransactions(1, 1); 117 | 118 | // assert 119 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/transactions"); 120 | expect(transactions).to.eql([{ amount: 123 }]); 121 | }); 122 | }); 123 | 124 | describe("#getFutureTransactions()", () => { 125 | it("should call correct endpoint", async () => { 126 | // arrange 127 | axiosStub = sandbox.stub().resolves({ 128 | data: { 129 | next: null, 130 | results: [{ 131 | amount: 123, 132 | }], 133 | total: 1, 134 | }, 135 | status: 200, 136 | }); 137 | client = new KontistClient(axiosStub); 138 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 139 | 140 | // act 141 | await client.getFutureTransactions(1); 142 | 143 | // assert 144 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/future-transactions"); 145 | }); 146 | }); 147 | 148 | describe("#getTransfers()", () => { 149 | it("should call correct endpoint", async () => { 150 | // arrange 151 | axiosStub = sandbox.stub().resolves({ 152 | data: { 153 | next: null, 154 | results: [{ 155 | amount: 123, 156 | }], 157 | total: 1, 158 | }, 159 | status: 200, 160 | }); 161 | client = new KontistClient(axiosStub); 162 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 163 | 164 | // act 165 | await client.getTransfers(1); 166 | 167 | // assert 168 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/transfer"); 169 | }); 170 | }); 171 | 172 | describe("#initiateTransfer()", () => { 173 | it("should call correct endpoint", async () => { 174 | // arrange 175 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 176 | 177 | // act 178 | await client.initiateTransfer(1, "mock recipient", "DE1234567890", 100, "mock"); 179 | 180 | // assert 181 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/transfer", "post", 182 | { recipient: "mock recipient", iban: "DE1234567890", amount: 100, note: "mock" }); 183 | }); 184 | }); 185 | 186 | describe("#confirmTransfer()", () => { 187 | it("should call correct endpoint", async () => { 188 | // arrange 189 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 190 | 191 | // act 192 | await client.confirmTransfer(1, "tid", "token", "mock recipient", "DE1234567890", 100, "mock"); 193 | 194 | // assert 195 | sinon.assert.calledWith(spyOnRequest, "/api/accounts/1/transfer/tid", "put", { 196 | amount: 100, 197 | authorizationToken: "token", 198 | iban: "DE1234567890", 199 | note: "mock", 200 | recipient: "mock recipient", 201 | }); 202 | }); 203 | }); 204 | 205 | describe("#getStatement()", () => { 206 | it("should call correct endpoint", async () => { 207 | // arrange 208 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 209 | 210 | // act 211 | await client.getStatement("2017", "02"); 212 | 213 | // assert 214 | sinon.assert.calledWith(spyOnRequest, "/api/user/statements/2017/02"); 215 | }); 216 | }); 217 | 218 | describe("#login()", () => { 219 | it("should call correct endpoint", async () => { 220 | // arrange 221 | const spyOnRequest = sinon.stub(client as any, "request" as any).callThrough(); 222 | 223 | // act 224 | await client.login("user", "password"); 225 | 226 | // assert 227 | sinon.assert.calledWith(spyOnRequest, "/api/user/auth-token", "post", 228 | { email: "user", password: "password" }); 229 | }); 230 | }); 231 | 232 | describe("#request", () => { 233 | it("should create request", async () => { 234 | // arrange 235 | 236 | // act 237 | await client.getUser(); 238 | 239 | // assert 240 | sinon.assert.calledWith( 241 | axiosStub, 242 | { 243 | data: undefined, 244 | headers: { 245 | "Content-Type": "application/json", 246 | "accept": "application/vnd.kontist.transactionlist.v2.1+json", 247 | }, 248 | maxRedirects: 0, 249 | method: "get", 250 | url: "https://api.kontist.com/api/user", 251 | }, 252 | ); 253 | }); 254 | it("should add Authorization header after login", async () => { 255 | // arrange 256 | Object.assign(client, { token: "TEST-TOKEN" }); 257 | 258 | // act 259 | await client.getUser(); 260 | 261 | // assert 262 | sinon.assert.calledWith( 263 | axiosStub, 264 | { 265 | data: undefined, 266 | headers: { 267 | "Authorization": "Bearer TEST-TOKEN", 268 | "Content-Type": "application/json", 269 | "accept": "application/vnd.kontist.transactionlist.v2.1+json", 270 | }, 271 | maxRedirects: 0, 272 | method: "get", 273 | url: "https://api.kontist.com/api/user", 274 | }, 275 | ); 276 | }); 277 | }); 278 | 279 | describe("#handleResponse", () => { 280 | it("should handle valid response", async () => { 281 | // arrange 282 | const resolveCallback = sinon.spy(); 283 | axiosStub = sandbox.stub().resolves({ data: { mock: "test" }, status: 200, statusText: "mock" }); 284 | client = new KontistClient(axiosStub); 285 | 286 | // act 287 | await client.getUser(); 288 | 289 | // assert 290 | resolveCallback.calledWith({ mock: "test" }); 291 | }); 292 | it("should handle invalid response", async () => { 293 | // arrange 294 | axiosStub = sandbox.stub().resolves({ status: 400, statusText: "mock" }); 295 | client = new KontistClient(axiosStub); 296 | 297 | // act 298 | try { 299 | await client.getUser(); 300 | throw new Error("client.getUser should have thrown an error"); 301 | } catch (e) { 302 | expect(e).to.be.instanceof(Error); 303 | expect(e.message).to.eql("mock"); 304 | } 305 | }); 306 | it("should handle invalid network response", async () => { 307 | // arrange 308 | axiosStub = sandbox.stub().rejects(new Error("network outage")); 309 | client = new KontistClient(axiosStub); 310 | 311 | // act 312 | try { 313 | await client.getUser(); 314 | throw new Error("client.getUser should have thrown an error"); 315 | } catch (e) { 316 | expect(e).to.be.instanceof(Error); 317 | expect(e.message).to.eql("network outage"); 318 | } 319 | }); 320 | }); 321 | }); 322 | --------------------------------------------------------------------------------