Automate your personal finances – for free, with no ads, and no data collection.
-## Quickstart
+
-**Prerequisites:** `node` (tested `v11.6.0`), `yarn` (tested `v1.10.0`)
+Mintable helps you:
-1. If you plan on using Plaid to fetch account data, [sign up](https://dashboard.plaid.com/signup) for an account and [apply for the development plan](https://plaid.com/pricing/). It usually takes them 1-2 business days to approve this request.
-2. Link your accounts and a spreadsheet to Mintable. Run these commands to walk through the setup:
+- Keep track of your account balances
+- Aggregate transactions from all your banking institutions, including checking accounts, savings accounts, and credit cards
+- Analyze and budget your spending using a spreadsheet and formulas
-```bash
-git clone https://github.com/kevinschaich/mintable.git
-cd mintable
-yarn
-yarn setup
-```
+![](./docs/img/mintable.png)
-3. After completing the setup, run the following at any time to populate updated data into your spreadsheet:
+
-```
-yarn mintable
-```
+[![](https://img.shields.io/travis/com/kevinschaich/mintable/master.svg)](https://travis-ci.com/kevinschaich/mintable)
+[![](https://img.shields.io/github/release/kevinschaich/mintable.svg)](https://github.com/kevinschaich/mintable/releases)
+[![](https://img.shields.io/github/license/kevinschaich/mintable.svg)](https://github.com/kevinschaich/mintable/blob/master/LICENSE)
+[![](https://img.shields.io/github/issues/kevinschaich/mintable.svg)](https://github.com/kevinschaich/mintable/issues)
+[![](https://img.shields.io/github/issues-pr/kevinschaich/mintable.svg)](https://github.com/kevinschaich/mintable/pulls)
+[![](https://img.shields.io/reddit/subreddit-subscribers/Mintable?style=social)](https://reddit.com/r/Mintable)
-> **Note**: If you started using Mintable before `v1.0.0`, you can run `yarn migrate` to migrate to the new web-based configuration framework.
+---
-## Overview
+## Quickstart
-![Mintable](./src/static/mintable.png)
+1. Sign up for [Plaid's Free Plan](https://plaid.com/pricing/).
+2. Install Mintable:
-Mintable simplifies managing your finances, for free, without ads, and without tracking your information. Here's how it works:
+ ```bash
+ npm install -g mintable
+ mintable setup
+ ```
-1. You connect your accounts and a spreadsheet to Mintable.
-1. Mintable integrates with financial institutions to automatically populate transactions in your spreadsheet.
-1. You can add whatever formulas, charts, or calculations you want (just like a normal spreadsheet). We also have templates to get you started.
+3. Update your account balances/transactions:
-## Features
+ ```
+ mintable fetch
+ ```
-- Locally hosted, open-source, 100% free, ad-free, no personal data tracking, no data stored by Mintable on central servers
-- Integrates with your financial institutions for fully-automated spreadsheet updates
-- Web based setup wizard and configuration framework:
+> **Note:** If you're already a version `1.x.x` user, you can [migrate your existing configuration to version `2.x.x`](./docs/README.md#migrating-from-v1xx).
-![Setup Wizard](./src/static/setup.png)
+## Documentation
-You can see a full list of options in the **[Config Docs](./docs/CONFIG.md)**.
+Check out the full documentation [in the `./docs` folder](./docs/README.md).
## FAQs
-**It's not working / I'm having trouble / I need help**
+**WTF is 'Mintable'?!**
-- [File an issue](https://github.com/kevinschaich/mintable/issues) or reach out on our [Reddit community](https://www.reddit.com/r/Mintable/).
+> **min·ta·ble**: _noun._
+> 1. An open-source tool to automate your personal finances – for free, with no ads, and no data collection. Derived from *mint* (the [wildly popular personal finance app from Intuit](https://www.mint.com/)) + *table* (a spreadsheet).
-**How is this different from [build-your-own-mint](https://github.com/yyx990803/build-your-own-mint)?**
+**Do I have to use Plaid?**
-- **[build-your-own-mint](https://github.com/yyx990803/build-your-own-mint)** is a set of scripts which solely facilitates the integration between Plaid and Google Sheets. It makes no assumptions about what you want your spreadsheet to look like, and you have to define your own logic to map transactions to spreadsheet updates.
-- **[Mintable](#)** is and end-to-end system that works out of the box. It comes with a setup wizard, a web-based configuration server, [pluggable providers](./docs/PROVIDERS.md) (you're not limited to just Plaid & Google Sheets), and a spreadsheet template.
+Nope. You can [import transactions from a CSV bank statement](./docs/README.md#manually--on-your-local-machine--via-csv-bank-statements) exclusively on your local machine. We also have [templates](./docs/templates) to get you started.
-**Do I have to give my data to Plaid and Google? Are there any completely self-hosted alternatives I can use?**
+**Do I have to use Google Sheets?**
-- It's [pluggable](./docs/PROVIDERS.md)! Plaid & Google Sheets are working right now – contributions are welcome for [other providers](./docs/PROVIDERS.md)!
+Nope. You can [export your account balances & transactions to a CSV file](./docs/README.md#on-your-local-machine--via-csv-files) exclusively on your local machine.
**Do I have to manually run this every time I want new transactions in my spreadsheet?**
-- You can **[Automate Updates with a CI Provider](./docs/CONFIG.md#automate-updates-with-a-ci-provider)** to get free, automated updates!
+Nope. You can automate it for free using [BitBar](./docs/README.md#automatically-in-your-macs-menu-bar--via-bitbar), [`cron`](./docs/README.md#automatically-in-your-local-machines-terminal--via-cron), or [GitHub Actions](./docs/README#automatically-in-the-cloud--via-github-actions).
+
+**It's not working!**
+
+- [File an issue](https://github.com/kevinschaich/mintable/issues) or [![](https://img.shields.io/reddit/subreddit-subscribers/Mintable?style=social)](https://reddit.com/r/Mintable).
-## Credits
+## Alternatives
-Mintable initially started as a fork of [Evan You](https://github.com/yyx990803)'s [build-your-own-mint](https://github.com/yyx990803/build-your-own-mint).
+- [**Money in Excel**](https://www.microsoft.com/en-us/microsoft-365/blog/2020/06/15/introducing-money-excel-easier-manage-finances/): Recently announced partnership between Microsoft/Plaid. Requires a Microsoft 365 subscription ($70+/year).
+- [**Mint**](https://www.mint.com/): Owned by Intuit (TurboTax). Apps for iOS/Android/Web.
+- [**build-your-own-mint**](https://github.com/yyx990803/build-your-own-mint): Some assembly required. More flexible.
diff --git a/docs/CONFIG.md b/docs/CONFIG.md
deleted file mode 100644
index 5c0e5a21..00000000
--- a/docs/CONFIG.md
+++ /dev/null
@@ -1,267 +0,0 @@
-# Configuration
-
-All configurations below can be made using the web configuration framework or by editing `mintable.config.json`.
-
-`mintable.config.json` is the secret sauce – it contains all of your private tokens and is never sent to third-party servers. This file is ignored by Git – keep a backup somewhere safe.
-
-> **Pro Tip:** You can use Dropbox or another trusted service to sync `mintable.config.json` across your machines. Run `ln -s /mintable.config.json .` from the repo root to symlink Mintable to the cloud version.
-
-#### Table of Contents
-
-- [General configuration](#general-configuration)
-- [Fetching Balances](#fetching-balances)
-- [Fetching Transactions](#fetching-transactions)
-- [Plaid](#plaid)
-- [Google Sheets](#google-sheets)
-
-## General configuration
-
-#### Debug Mode
-
-`DEBUG` mode logs the output of each API call and function to the console.
-
-**Default:**
-
-```javascript
-"DEBUG": undefined // If unspecified, defaults to false
-```
-
-If you want to enable debug mode, you can add the following line to your `mintable.config.json` file:
-
-```javascript
-"DEBUG": true
-```
-
-#### Host
-
-`HOST` specifies the host for Mintable's setup server.
-
-**Default:**
-
-```javascript
-"HOST": "localhost"
-```
-
-For example, if you want to run Mintable on a custom server and listen on `0.0.0.0`, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"HOST": "0.0.0.0"
-```
-
-#### Port
-
-`PORT` specifies the port for Mintable's setup server.
-
-**Default:**
-
-```javascript
-"PORT": 3000
-```
-
-For example, if you already have an application running on port `3000` and instead want to use port `8080`, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"PORT": "8080"
-```
-
-#### Account Provider
-
-`ACCOUNT_PROVIDER` specifies which service to use to fetch transactions.
-
-**Default:**
-
-```javascript
-"ACCOUNT_PROVIDER": "plaid"
-```
-
-#### Spreadsheet Provider
-
-`SHEET_PROVIDER` specifies which service to use to automate spreadsheet updates.
-
-**Default:**
-
-```javascript
-"SHEET_PROVIDER": "sheets" // "sheets" = Google Sheets
-```
-
-#### Automate Updates with a CI Provider
-
-This repo includes config files for both [CircleCI](https://circleci.com/) and [Travis CI](https://travis-ci.com) to run builds automatically.
-
-Most CI providers allow you to set **environment variables** to configure sensitive information (like the stuff in `mintable.config.json`). We've included a handy script to get that set up:
-
-```
-yarn export
-```
-
-Run this command and paste the result into an environment variable called `MINTABLE_CONFIG` in your CI provider of choice. Mintable will handle the rest.
-
-> **Note:** Some CI providers (like Travis) require you to wrap this variable in single quotes, i.e. `'{ "ACCOUNT_PROVIDER": "plaid", ...}'`. If you get an error similar to `Unable to parse JSON...` when you run your CI build, give this a try.
-
-> **Warning:** If you choose to use CircleCI, you should turn off **Pass secrets to builds from forked pull requests** under **Build Settings** > **Advanced Settings**.
-
-## Fetching Balances
-
-#### Create Balances Sheet
-
-`CREATE_BALANCES_SHEET` optionally fetches the balances of all your connected accounts and places them in a sheet called `Balances`.
-
-**Default:**
-
-```javascript
-"CREATE_BALANCES_SHEET": undefined // If unspecified, defaults to false
-```
-
-If you want to enable this, you can add the following line to your `mintable.config.json` file:
-
-```javascript
-"CREATE_BALANCES_SHEET": true
-```
-
-#### Balance Columns
-
-`BALANCE_COLUMNS` specifies a list of account properties (using [`_.get()` syntax](https://lodash.com/docs/4.17.11#get)) to automatically update in your `Balances` spreadsheet. All the contents of these columns will be cleared and overwritten each time you run Mintable.
-
-**Default:**
-
-```javascript
-"BALANCE_COLUMNS": ['name', 'official_name', 'type', 'balances.available', 'balances.current', 'balances.limit']
-```
-
-For example, if you only want to auto-populate the name and amount for each account, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"TRANSACTION_COLUMNS": ["name", "balances.current"]
-```
-
-## Fetching Transactions
-
-#### Start Date
-
-`START_DATE` specifies the lower bound for fetching transactions in `YYYY.MM.DD` format.
-
-**Default:**
-
-```javascript
-"START_DATE": undefined // If end date is not specified, Mintable will fetch the last 2 months of transactions
-```
-
-For example, if you only want to fetch transactions which occur after or on December 1, 2018, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"START_DATE": "2018.12.01"
-```
-
-#### End Date
-
-`END_DATE` specifies the upper bound for fetching transactions in `YYYY.MM.DD` format.
-
-**Default:**
-
-```javascript
-"END_DATE": undefined // If end date is not specified, Mintable will fetch up until the current date
-```
-
-For example, if you only want to fetch transactions which occur before or on December 1, 2018, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"END_DATE": "2018.12.01"
-```
-
-#### Transaction Columns
-
-`TRANSACTION_COLUMNS` specifies a list of transaction properties (using [`_.get()` syntax](https://lodash.com/docs/4.17.11#get)) to automatically update in your spreadsheet. All the contents of these columns will be cleared and overwritten each time you run Mintable.
-
-**Default:**
-
-```javascript
-"TRANSACTION_COLUMNS": [ 'date', 'amount', 'name', 'account_details.official_name', 'category.0', 'category.1', 'pending' ]
-```
-
-For example, if you only want to auto-populate the name and amount for each transaction, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"TRANSACTION_COLUMNS": ["name", "amount"]
-```
-
-#### Reference Columns
-
-`REFERENCE_COLUMNS` specifies a list of additional, non-automated columns for your reference/bookkeeping purposes. Each time you run Mintable, the contents of these columns will be preserved.
-
-**Default:**
-
-```javascript
-"REFERENCE_COLUMNS": ['notes', 'work', 'joint']
-```
-
-For example, if you want to add one column to track work expenses, and another to track joint expenses shared with a partner, you could add the following line to your `mintable.config.json` file:
-
-```javascript
-"REFERENCE_COLUMNS": ["work", "joint"]
-```
-
-> **Warning:** Since reference columns are not automated by Mintable, they have the potential to get out of sync with transaction data (for example, if your bank deletes a transaction, causing a row to get removed in `TRANSACTION_COLUMNS`)
-
-# Provider-Specific Configuration
-
-You can see the API definitions for account & spreadsheet providers in the **[provider docs](./docs/PROVIDERS.md)**.
-
-## Plaid
-
-#### Category Overrides
-
-`CATEGORY_OVERRIDES` specifies a list of overrides to handle transactions that are routinely miscategorized by Plaid's servers.
-
-**Default:**
-
-```javascript
-"CATEGORY_OVERRIDES": []
-```
-
-Overrides take the following format:
-
-* `pattern`: [JavaScript Regular Expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Syntax) to test transaction names against
-* `flags`: [JavaScript Regular Expression flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#Syntax) (i.e. `i` for case insensitive)
-* `category.0`: Override for first (top-level) category
-* `category.1`: Override for second (level-2) category
-
-For example, if you want anything matching `autopay` or `e-payment` to get categorized as `Credit Card Payment`, you could add the following lines to your `mintable.config.json` file:
-
-```javascript
-"CATEGORY_OVERRIDES": [
- {
- "pattern": ".*(autopay|e.payment).*",
- "flags": "i",
- "category.0": "Transfer",
- "category.1": "Credit Card Payments"
- }
-]
-```
-
-## Google Sheets
-
-#### Template Sheet
-
-`TEMPLATE_SHEET` specifies the template spreadsheet to use when creating a _new_ sheet for a month.
-
-**Default:**
-
-```javascript
-"TEMPLATE_SHEET": {
- // Public template: https://docs.google.com/spreadsheets/d/10fYhPJzABd8KlgAzxtiyFN-L_SebTvM8SaAK_wHk-Fw
- "SHEET_ID": "10fYhPJzABd8KlgAzxtiyFN-L_SebTvM8SaAK_wHk-Fw",
- "SHEET_TITLE": "Template"
-}
-```
-
-* `SHEET_ID`: Google Sheets spreadsheet ID (from the URL: `docs.google.com/spreadsheets/d/`**`sheet_id`**`/edit`)
-* `SHEET_TITLE`: Title of the sheet (along the bottom row of the document)
-
-For example, you could add the following lines to your `mintable.config.json` file:
-
-```javascript
-"TEMPLATE_SHEET": {
- "SHEET_ID": "10fYhPJzABd8KasbqiyFN-L_SebTvM8SaAK_wHk-Fw",
- "SHEET_TITLE": "My Template Sheet"
-}
-```
diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md
deleted file mode 100644
index 7f54cfdf..00000000
--- a/docs/PROVIDERS.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Providers APIs
-
-Mintable is designed to be pluggable, i.e. you can swap out Plaid or Google Sheets for another service of your choice.
-
-This document outlines what Mintable expects of a provider, and what constraints/functionality you need to adhere to/implement if you want to add a new provider.
-
-## Transactions (`ACCOUNT_PROVIDER`) API
-
-Account providers should provide an exported function which takes in a startDate and endDate, and returns a `Promise` of a raw list of transaction objects, i.e.:
-
-```javascript
-transactions = await require('../lib/providerName').fetchTransactions(startDate, endDate)
-```
-
-At minimum, we expect `name`, `date`, and `amount` to be defined.
-
-For example, the following would an acceptable response (after promise resolution):
-
-```javascript
-[
- { "name": "Amazon.com", "date": "2019-04-16T07:00:00.000Z", "amount": -40.22 },
- { "name": "United Airlines", "date": "2019-04-06T07:00:00.000Z", "amount": -500 },
- { "name": "Uber", "date": "2019-04-04T07:00:00.000Z", "amount": -6.33 }
-]
-```
-
-## Spreadsheets (`SHEET_PROVIDER`) API
-
-Spreadsheet providers should provide an exported function which takes in a map of sheet name to a list of transactions for that sheet, and return a `Promise` which resolves when all necessary operations to update that sheet are complete, i.e.:
-
-```javascript
-await require('../lib/providerName').updateSheets(updates, options)
-```
-
-where `updates` come in the following format:
-
-```javascript
-{
- "2019.04": [
- { "name": "Amazon.com", "date": "2019.04.16", "amount": -40.22 },
- { "name": "United Airlines", "date": "2019.04.26", "amount": -500 }
- ],
- "2019.05": [
- { "name": "Uber", "date": "2019.05.11", "amount": -6.33 }
- ]
-}
-```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..8367e29c
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,331 @@
+# Documentation
+
+#### Table of Contents
+
++ [Overview](#overview)
++ [Installation](#installation)
+ + [Creating a Fresh Installation](#creating-a-fresh-installation)
+ + [Migrating from `v1.x.x`](#migrating-from-v1xx)
++ [Importing Account Balances & Transactions](#importing-account-balances--transactions)
+ + [Automatically – in the cloud – via Plaid](#automatically-in-the-cloud--via-plaid)
+ + [Manually – on your local machine – via CSV bank statements](#manually--on-your-local-machine--via-csv-bank-statements)
++ [Exporting Account Balances & Transactions](#exporting-account-balances--transactions)
+ + [In the cloud – via Google Sheets](#in-the-cloud-via-google-sheets)
+ + [On your local machine – via CSV files](#on-your-local-machine--via-csv-files)
++ [Updating Transactions/Accounts](#updating-transactionsaccounts)
+ + [Manually – in your local machine's terminal](#manually-in-your-local-machines-terminal)
+ + [Automatically – in your Mac's Menu Bar – via BitBar](#automatically-in-your-macs-menu-bar--via-bitbar)
+ + [Automatically – in your local machine's terminal – via `cron`](#automatically-in-your-local-machines-terminal--via-cron)
+ + [Automatically – in the cloud – via GitHub Actions](#automatically-in-the-cloud--via-github-actions)
++ [Transaction Rules](#transaction-rules)
+ + [Transaction `filter` Rules](#transaction-filter-rules)
+ + [Transaction `override` Rules](#transaction-override-rules)
++ [Development](#development)
++ [Contributing](#contributing)
+
+## Overview
+
+![Mintable](./img/mintable.png)
+
+Mintable simplifies managing your finances, for free, without ads, and without tracking your information. Here's how it works:
+
+1. You connect your accounts and a spreadsheet to Mintable.
+1. Mintable integrates with your financial institutions to automatically populate transactions into the leftmost columns in your spreadsheet.
+1. You can add whatever formulas, charts, or calculations you want to the right of your transactions (just like a normal spreadsheet). We also have templates to get you started.
+
+---
+
+## Installation
+
+### Creating a Fresh Installation
+
+1. Sign up for [Plaid's Free Plan](https://plaid.com/pricing/). The free plan is limited to 100 banking institutions which should be more than enough for personal use. After applying and verifying your email it usually takes a day or two for them to approve your account.
+2. Install the global `mintable` command line utility:
+
+ ```bash
+ npm install -g mintable
+ ```
+
+3. Set up the integration with your banks and a spreadsheet using the setup wizard:
+
+ ```bash
+ mintable setup
+ ```
+
+4. Update your account balances/transactions:
+
+ ```
+ mintable fetch
+ ```
+
+![Mintable CLI](./img/cli.png)
+
+### Migrating from `v1.x.x`
+
+> **⚠️ Warning:** Plaid [introduced a breaking change in July 2020](https://github.com/plaid/plaid-node/pull/310) which deprecates the Public Key component from the authentication process. Once you upgrade to `v2.x.x` and disable your Public Key, you will no longer be able to continue using your `v1.x.x` installation. Proceed with caution.
+
+1. [Disable the Public Key in your Plaid Dashboard](https://plaid.com/docs/upgrade-to-link-tokens/#disable-the-public-key) (read ⚠️ above!)
+
+2. Install the new `v2.x.x` `mintable` command line utility:
+
+ ```bash
+ npm install -g mintable
+ ```
+
+3. Migrate your config to the new format:
+
+ ```bash
+ mintable migrate --old-config-file /path/to/your/old/mintable.config.json
+ ```
+
+4. Update your account balances/transactions:
+
+ ```bash
+ mintable fetch
+ ```
+
+> **Note:** After successful migration you can delete everything in your `v1.x.x` `mintable` folder. You may want to keep a copy of your `mintable.config.json` for posterity.
+
+---
+
+## Importing Account Balances & Transactions
+
+### Automatically – in the cloud – via [Plaid](https://plaid.com)
+
+You can run:
+
+```bash
+mintable plaid-setup
+```
+
+to enter the Plaid setup wizard. This will allow you to automatically fetch updated account balances/transactions from your banking institutions every time `mintable fetch` is run.
+
+After you have the base Plaid integration working, you can run:
+
+```bash
+mintable account-setup
+```
+
+to enter the account setup wizard to add, update, or remove accounts.
+
+![Account Setup](./img/account-setup.png)
+
+This will launch a local web server (necessary to authenticate with Plaid's servers) for you to connect your banks.
+
+To add a new account, click the blue **Link A New Account** button. To re-authenticate with an existing account, click the blue **Update** button next to the account name in the table.
+
+> **Note:** Plaid is the default import integration and these steps are not necessary if you've already run `mintable setup`.
+
+### Manually – on your local machine – via CSV bank statements
+
+You can run:
+
+```bash
+mintable csv-import-setup
+```
+
+to enter the CSV import setup wizard. This will allow you to manually import files or globs (`path/to/my/folder/transactions/*.csv`) every time `mintable fetch` is run.
+
+You'll need to define a transformer to map properties in your source CSV spreadsheet to valid Mintable transaction properties, and a valid date format.
+
+We have a number of templates available for popular financial institutions to get you started:
+
+- [Apple Card](./templates/apple-card.json)
+- [Discover Card](./templates/discover-card.json)
+- [Venmo](./templates/venmo.json)
+- [Chase](./templates/chase.json)
+- [American Express](./templates/american-express.json)
+- [Rogers Bank Credit Card](./templates/rogers-bank-credit-card.json)
+
+These templates can be added into the `accounts` section of your `mintable.jsonc` configuration file.
+
+> **Note:** CSV Imports do not support account balances.
+
+---
+
+## Exporting Account Balances & Transactions
+
+### In the cloud – via [Google Sheets](https://www.google.com/sheets/about/)
+
+You can run:
+
+```bash
+mintable google-setup
+```
+
+to enter the Google Sheets setup wizard. This will allow you to automatically update a sheet with your transactions/account balances every time `mintable fetch` is run.
+
+> **Note:** Google Sheets is the default export integration and this step is not necessary if you've already run `mintable setup`.
+
+### On your local machine – via CSV files
+
+You can run:
+
+```bash
+mintable csv-export-setup
+```
+
+to enter the CSV export setup wizard. This will allow you to manually export a CSV containing your transactions/account balances every time `mintable fetch` is run.
+
+---
+
+## Updating Transactions/Accounts
+
+### Manually – in your local machine's terminal
+
+After you have connected a banking institution, you can run:
+
+```bash
+mintable fetch
+```
+
+to automate updates to your spreadsheet.
+
+### Automatically – in your Mac's Menu Bar – via [BitBar](https://github.com/matryer/bitbar#get-started)
+
+You can put Mintable in your Mac's menu bar, and have it run automatically every hour using our [BitBar Plugin](https://github.com/matryer/bitbar-plugins/pull/1460).
+
+![BitBar](./img/bitbar.png)
+
+1. [Install BitBar](https://github.com/matryer/bitbar/releases) on your Mac.
+2. Set your plugin folder.
+3. Create a new file in `mintable.1h.zsh` in your plugin folder.
+4. Copy & paste [this](https://github.com/matryer/bitbar-plugins/blob/39e8f252ed69d0dd46bbe095299e52279e86d737/Finance/mintable.1h.zsh) into the file you just created and save.
+5. Open **BitBar** > **Preferences** > **Refresh All** to update your spreadsheet.
+
+> **Note:** The plugin above is pending approval and this install process should be much easier moving forward.
+
+### Automatically – in your local machine's terminal – via `cron`
+
+You can run Mintable automatically within your terminal using `cron`:
+
+![`cron`](./img/cron.png)
+
+```bash
+echo "0 * * * * export PATH="/usr/local/bin:$PATH" && mintable fetch" > ~/mintable.cron
+crontab ~/mintable.cron
+```
+
+The first step creates a new file `~/mintable.cron` which contains an interval and the command you want to run. The second step registers that file with `crontab`, the command-line executable which actually schedules the job with your operating system.
+
+The default refresh interval is 1 hour – you can use [Crontab Guru](https://crontab.guru/) to define your own interval.
+
+You can remove this schedule by running:
+
+```bash
+crontab -r
+```
+
+> **Note:** The instructions above assume your global `mintable` CLI lives in `/usr/local/bin`, but if your installation path is different (run `which mintable`) you should use that instead.
+
+### Automatically – in the cloud – via GitHub Actions
+
+You can use GitHub Actions to run Mintable automatically in the cloud:
+
+![GitHub Actions](./img/github-actions.png)
+
+1. Fork [this repo](https://github.com/kevinschaich/mintable).
+2. Go to your repo's **Actions** > Click **I understand my workflows, go ahead and enable them**
+3. Go to your repo's **Settings** > **Secrets** and add a **New Secret**.
+4. Name the secret `MINTABLE_CONFIG`, and copy and paste the full contents of your `~/mintable.jsonc` file into the body of the secret.
+5. In your repo's `./.github/workflows/fetch.yml`, uncomment the following block and commit the changes:
+
+ ```
+ # schedule:
+ # - cron: '0 * * * *'
+ ```
+
+In the **Actions** tab of your repo, the **Fetch** workflow will now update your sheet periodically. The default refresh interval is 1 hour – you can use [Crontab Guru](https://crontab.guru/) to define your own interval.
+
+> **Note:** The minimum interval supported by GitHub Actions is every 5 minutes.
+
+---
+
+## Transaction Rules
+
+### Transaction `filter` Rules
+
+Transaction `filter` rules allow you to exclude transactions from your spreadsheet based on a set of [conditions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).
+
+For example, if you wanted to exclude any cross-account transfers, you might add the following to your `transactions.rules` config:
+
+```json
+"rules": [
+ {
+ "conditions": [
+ {
+ "property": "name",
+ "pattern": "(transfer|xfer|trnsfr)",
+ "flags": "ig"
+ }
+ ],
+ "type": "filter"
+ }
+]
+```
+
+### Transaction `override` Rules
+
+Transaction `override` rules allow you to override auto-populated fields based on a set of [conditions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), search for a [pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions), and replace it with another [pattern](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).
+
+You might want to do this to standardize values between financial institutions (`XFER` -> `Transfer`), or tune things to suit your particular budgeting needs (described below).
+
+For example, let's say you want to know how much you are spending on coffee each month, but Plaid/your bank categorizes your favorite shops as `Restaurants – Fast Food`. You might add the following to your `transactions.rules` config:
+
+```json
+"rules": [
+ {
+ "conditions": [
+ {
+ "property": "name",
+ "pattern": "(dunkin|starbucks|peets|philz)",
+ "flags": "ig"
+ }
+ ],
+ "type": "override",
+ "property": "category",
+ "findPattern": "Fast Food",
+ "replacePattern": "Coffee Shops",
+ "flags": "i"
+ }
+]
+```
+
+When you run `mintable fetch` the next time, the category would be `Restaurants – Coffee Shops`.
+
+## Development
+
+To get started:
+
+```bash
+git clone https://github.com/kevinschaich/mintable
+cd mintable
+
+npm install
+npm run build
+npm link
+```
+
+The global `mintable` command will now point to your local version (`/lib/scripts/cli.js`). To start compilation in watch mode:
+
+```bash
+npm run watch
+```
+
+To publish a new version, increment `version` in `package.json` and run:
+
+```bash
+npm run build
+npm publish
+```
+
+To revert to the production release of `mintable`, run:
+
+```bash
+npm unlink
+npm install -g mintable
+```
+
+## Contributing
+
+Before posting please check if your issue has already been reported. We'll gladly accept PRs, feature requests, or bugs via [Issues](https://github.com/kevinschaich/mintable/issues).
diff --git a/docs/css/account-setup.css b/docs/css/account-setup.css
new file mode 100644
index 00000000..e1b86aa6
--- /dev/null
+++ b/docs/css/account-setup.css
@@ -0,0 +1,119 @@
+body {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+h1,
+h2,
+h3,
+p,
+th,
+tr,
+button {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ text-rendering: optimizeLegibility;
+}
+
+#logo {
+ width: 150px;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+h1 {
+ font-weight: 500;
+ font-size: 60px;
+ margin-top: 40px;
+ color: #333;
+}
+
+h2 {
+ font-weight: 300;
+ margin-top: 40px;
+ color: #333;
+}
+
+h3 {
+ font-size: 18px;
+ font-weight: 500;
+}
+
+th {
+ font-weight: 700;
+}
+
+td {
+ font-weight: 400;
+ color: #333;
+}
+
+#accounts {
+ margin-top: 80px;
+}
+
+#accounts-table {
+ border-radius: 5px;
+ background: rgb(245, 245, 245);
+ border: 1px solid rgb(235, 235, 235);
+}
+
+p {
+ padding: 10px 40px;
+}
+
+th {
+ padding: 20px;
+}
+
+td {
+ padding: 10px 20px;
+ border-top: 1px solid rgb(235, 235, 235);
+ background: #fff;
+}
+
+button {
+ font-size: 18px;
+ font-weight: 500;
+ border-radius: 5px;
+ padding: 10px 40px;
+ cursor: pointer;
+ background-color: transparent;
+ border: 1px solid #0a85ea;
+ color: #0a85ea;
+ transition: all 300ms;
+}
+
+button:hover {
+ color: white;
+ background-color: #0a85ea;
+}
+
+button.remove {
+ border: 1px solid #ed0d3a;
+ color: #ed0d3a;
+}
+
+button.remove:hover {
+ color: white;
+ background-color: #ed0d3a;
+}
+
+#link-button {
+ margin-top: 80px;
+}
+
+#done-button {
+ border: 1px solid #2abf54;
+ color: #2abf54;
+ margin-top: 40px;
+}
+
+#done-button:hover {
+ color: white;
+ background-color: #2abf54;
+}
diff --git a/docs/img/account-setup.png b/docs/img/account-setup.png
new file mode 100644
index 00000000..3a2b9ea1
Binary files /dev/null and b/docs/img/account-setup.png differ
diff --git a/docs/img/bitbar.png b/docs/img/bitbar.png
new file mode 100644
index 00000000..4952d567
Binary files /dev/null and b/docs/img/bitbar.png differ
diff --git a/docs/img/cli.png b/docs/img/cli.png
new file mode 100644
index 00000000..2e45d7e4
Binary files /dev/null and b/docs/img/cli.png differ
diff --git a/docs/img/cron.png b/docs/img/cron.png
new file mode 100644
index 00000000..417ac2c1
Binary files /dev/null and b/docs/img/cron.png differ
diff --git a/docs/img/github-actions.png b/docs/img/github-actions.png
new file mode 100644
index 00000000..82671608
Binary files /dev/null and b/docs/img/github-actions.png differ
diff --git a/src/static/icon.png b/docs/img/icon.png
similarity index 100%
rename from src/static/icon.png
rename to docs/img/icon.png
diff --git a/src/static/logo.png b/docs/img/logo.png
similarity index 100%
rename from src/static/logo.png
rename to docs/img/logo.png
diff --git a/src/static/mintable.png b/docs/img/mintable.png
similarity index 100%
rename from src/static/mintable.png
rename to docs/img/mintable.png
diff --git a/docs/templates/american-express.json b/docs/templates/american-express.json
new file mode 100644
index 00000000..210e8c3d
--- /dev/null
+++ b/docs/templates/american-express.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "American Express": {
+ "paths": ["/path/to/my/american/express/statements/*.csv"],
+ "transformer": {
+ "Description": "name",
+ "Date": "date",
+ "Amount": "amount",
+ "Category": "category"
+ },
+ "dateFormat": "MM/dd/yyyy",
+ "id": "American Express",
+ "integration": "csv-import",
+ "negateValues": true
+ }
+ }
+}
diff --git a/docs/templates/apple-card.json b/docs/templates/apple-card.json
new file mode 100644
index 00000000..cec17d31
--- /dev/null
+++ b/docs/templates/apple-card.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "Apple Card": {
+ "paths": ["/path/to/my/apple/card/statements/*.csv"],
+ "transformer": {
+ "Merchant": "name",
+ "Transaction Date": "date",
+ "Amount (USD)": "amount",
+ "Category": "category"
+ },
+ "dateFormat": "MM/dd/yyyy",
+ "id": "Apple Card",
+ "integration": "csv-import",
+ "negateValues": true
+ }
+ }
+}
diff --git a/docs/templates/chase.json b/docs/templates/chase.json
new file mode 100644
index 00000000..a3739e25
--- /dev/null
+++ b/docs/templates/chase.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "Chase": {
+ "paths": ["/path/to/my/chase/statements/*.csv"],
+ "transformer": {
+ "Description": "name",
+ "Transaction Date": "date",
+ "Amount": "amount",
+ "Category": "category"
+ },
+ "dateFormat": "MM/dd/yyyy",
+ "id": "Chase",
+ "integration": "csv-import",
+ "negateValues": true
+ }
+ }
+}
diff --git a/docs/templates/discover-card.json b/docs/templates/discover-card.json
new file mode 100644
index 00000000..1558d9d2
--- /dev/null
+++ b/docs/templates/discover-card.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "Discover Card": {
+ "paths": ["/path/to/my/discover/card/statements/*.csv"],
+ "transformer": {
+ "Description": "name",
+ "Trans. Date": "date",
+ "Amount": "amount",
+ "Category": "category"
+ },
+ "dateFormat": "MM/dd/yyyy",
+ "id": "Discover Card",
+ "integration": "csv-import",
+ "negateValues": true
+ }
+ }
+}
diff --git a/docs/templates/rogers-bank-credit-card.json b/docs/templates/rogers-bank-credit-card.json
new file mode 100644
index 00000000..b4482cf7
--- /dev/null
+++ b/docs/templates/rogers-bank-credit-card.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "Rogers Bank Credit Card": {
+ "paths": ["/path/to/my/rogers/bank/credit/card/statements/*.csv"],
+ "transformer": {
+ "Merchant Name": "name",
+ "Date": "date",
+ "Amount": "amount",
+ "Merchant Category Description": "category"
+ },
+ "dateFormat": "yyyy/MM/dd",
+ "id": "Rogers Bank Credit Card",
+ "integration": "csv-import",
+ "negateValues": true
+ }
+ }
+}
diff --git a/docs/templates/venmo.json b/docs/templates/venmo.json
new file mode 100644
index 00000000..d9f59ae0
--- /dev/null
+++ b/docs/templates/venmo.json
@@ -0,0 +1,17 @@
+{
+ "accounts": {
+ "Venmo": {
+ "paths": ["/path/to/my/venmo/statements/*.csv"],
+ "transformer": {
+ "Note": "name",
+ "Datetime": "date",
+ "Amount (total)": "amount",
+ "Type+From+To": "category"
+ },
+ "dateFormat": "yyyy-MM-dd'T'HH:mm:ss",
+ "id": "Venmo",
+ "integration": "csv-import",
+ "negateValues": false
+ }
+ }
+}
diff --git a/package.json b/package.json
index c2aafaf9..73f91891 100644
--- a/package.json
+++ b/package.json
@@ -1,45 +1,80 @@
{
- "name": "mintable",
- "private": true,
- "main": "mintable.js",
- "author": "Kevin Schaich",
- "license": "MIT",
- "version": "1.2.5",
- "scripts": {
- "circleci:dry-run": "circleci local execute -e MINTABLE_CONFIG=$MINTABLE_CONFIG",
- "export": "node ./src/scripts/export.js",
- "format": "yarn prettier",
- "migrate": "node ./src/scripts/migrate.js",
- "mintable": "node ./src/scripts/mintable.js",
- "setup": "cd src && node ./scripts/setup.js",
- "prettier": "prettier --write ./src/**/*.{css,scss,less,js,jsx,ts,tsx,json} --ignore-path ./.gitignore"
- },
- "dependencies": {
- "@zeit/next-sass": "^1.0.1",
- "body-parser": "^1.18.3",
- "clipboardy": "^1.2.3",
- "date-fns": "^1.30.1",
- "dotenv": "^7.0.0",
- "express": "^4.16.4",
- "googleapis": "27",
- "indent-string": "^3.2.0",
- "isomorphic-unfetch": "^3.0.0",
- "lodash": "^4.17.11",
- "log-symbols": "^2.2.0",
- "next": "^8.0.3",
- "node-sass": "^4.11.0",
- "opn": "^6.0.0",
- "ora": "^3.4.0",
- "p-each-series": "^2.1.0",
- "p-map-series": "^2.1.0",
- "plaid": "^2.10.0",
- "promise-fs": "^2.1.0",
- "react": "^16.8.6",
- "react-dom": "^16.8.6",
- "react-icons": "^3.5.0",
- "react-plaid-link": "^1.2.0"
- },
- "devDependencies": {
- "prettier": "^1.16.4"
- }
+ "name": "mintable",
+ "author": "Kevin Schaich (http://kevinschaich.io)",
+ "license": "MIT",
+ "version": "2.0.0-beta.47",
+ "bin": "./lib/scripts/cli.js",
+ "preferGlobal": true,
+ "scripts": {
+ "build": "tsc",
+ "watch": "tsc --watch",
+ "lint": "prettier --check ./src/**/*.js ./src/**/*.html ./src/**/*.ts ./docs/**/*.json",
+ "lint-fix": "prettier --write ./src/**/*.js ./src/**/*.html ./src/**/*.ts ./docs/**/*.json",
+ "test": "npm run build && npm run lint"
+ },
+ "files": [
+ "src",
+ "lib",
+ "docs",
+ "tsconfig.json"
+ ],
+ "dependencies": {
+ "@types/body-parser": "^1.19.0",
+ "@types/express": "^4.17.3",
+ "@types/glob": "^7.1.2",
+ "@types/lodash": "^4.14.149",
+ "@types/node": "^14.0.13",
+ "@types/prompts": "^2.0.3",
+ "ajv": "^6.12.0",
+ "body-parser": "^1.19.0",
+ "chalk": "^3.0.0",
+ "csv-parse": "^4.10.1",
+ "csv-stringify": "^5.5.0",
+ "date-fns": "^2.10.0",
+ "express": "^4.17.1",
+ "glob": "^7.1.6",
+ "googleapis": "47.0.0",
+ "jsonc": "^2.0.0",
+ "lodash": "^4.17.15",
+ "open": "^7.0.2",
+ "plaid": "^7.0.0",
+ "prompts": "^2.3.1",
+ "typescript": "^3.8.3",
+ "typescript-json-schema": "^0.42.0",
+ "yargs": "^15.1.0"
+ },
+ "devDependencies": {
+ "prettier": "^1.16.4"
+ },
+ "description": "Automate your personal finances – for free, with no ads, and no data collection.",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/kevinschaich/mintable.git"
+ },
+ "keywords": [
+ "finance",
+ "finance-management",
+ "personal-finance",
+ "mint",
+ "sheets-api",
+ "google-sheets",
+ "google-sheets-api",
+ "plaid",
+ "plaid-api",
+ "analytics",
+ "tracker",
+ "finance-tracker",
+ "personal-capital",
+ "spreadsheet",
+ "mintable",
+ "money",
+ "budget",
+ "budgeting",
+ "budget-management",
+ "javascript"
+ ],
+ "bugs": {
+ "url": "https://github.com/kevinschaich/mintable/issues"
+ },
+ "homepage": "https://github.com/kevinschaich/mintable#readme"
}
diff --git a/src/common/config.ts b/src/common/config.ts
new file mode 100644
index 00000000..0544140f
--- /dev/null
+++ b/src/common/config.ts
@@ -0,0 +1,203 @@
+import { IntegrationConfig, IntegrationId } from '../types/integrations'
+import { AccountConfig } from '../types/account'
+import { TransactionConfig } from '../types/transaction'
+import { logInfo, logError } from './logging'
+import { argv } from 'yargs'
+import * as fs from 'fs'
+import * as os from 'os'
+import { resolve, join } from 'path'
+import { Definition, CompilerOptions, PartialArgs, getProgramFromFiles, generateSchema } from 'typescript-json-schema'
+import Ajv from 'ajv'
+import { BalanceConfig } from '../types/balance'
+import { jsonc } from 'jsonc'
+
+const DEFAULT_CONFIG_FILE = '~/mintable.jsonc'
+const DEFAULT_CONFIG_VAR = 'MINTABLE_CONFIG'
+
+const DEFAULT_CONFIG: Config = {
+ accounts: {},
+ transactions: {
+ integration: IntegrationId.Google,
+ properties: ['date', 'amount', 'name', 'account', 'category']
+ },
+ balances: {
+ integration: IntegrationId.Google,
+ properties: ['institution', 'account', 'type', 'current', 'available', 'limit', 'currency']
+ },
+ integrations: {}
+}
+
+export interface FileConfig {
+ type: 'file'
+ path: string
+}
+
+export interface EnvironmentConfig {
+ type: 'environment'
+ variable: string
+}
+
+export type ConfigSource = FileConfig | EnvironmentConfig
+
+export interface Config {
+ integrations: { [id: string]: IntegrationConfig }
+ accounts: { [id: string]: AccountConfig }
+ transactions: TransactionConfig
+ balances: BalanceConfig
+}
+
+export const getConfigSource = (): ConfigSource => {
+ if (argv['config-file']) {
+ const path = argv['config-file'].replace(/^~(?=$|\/|\\)/, os.homedir())
+ logInfo(`Using configuration file \`${path}.\``)
+ return { type: 'file', path: path }
+ }
+
+ if (process.env[DEFAULT_CONFIG_VAR]) {
+ logInfo(`Using configuration variable '${DEFAULT_CONFIG_VAR}.'`)
+ return { type: 'environment', variable: DEFAULT_CONFIG_VAR }
+ }
+
+ // Default to DEFAULT_CONFIG_FILE
+ const path = DEFAULT_CONFIG_FILE.replace(/^~(?=$|\/|\\)/, os.homedir())
+ logInfo(`Using default configuration file \`${path}.\``)
+ logInfo(`You can supply either --config-file or --config-variable to specify a different configuration.`)
+ return { type: 'file', path: path }
+}
+
+export const readConfig = (source: ConfigSource, checkExists?: boolean): string => {
+ if (source.type === 'file') {
+ try {
+ const config = fs.readFileSync(source.path, 'utf8')
+ logInfo('Successfully opened configuration file.')
+ return config
+ } catch (e) {
+ if (checkExists) {
+ logInfo('Unable to open config file.')
+ } else {
+ logError('Unable to open configuration file.', e)
+ logInfo("You may want to run `mintable setup` (or `mintable migrate`) if you haven't already.")
+ }
+ }
+ }
+ if (source.type === 'environment') {
+ try {
+ const config = process.env[source.variable]
+
+ if (config === undefined) {
+ throw `Variable \`${source.variable}\` not defined in environment.`
+ }
+
+ logInfo('Successfully retrieved configuration variable.')
+ return config
+ } catch (e) {
+ if (!checkExists) {
+ logInfo('Unable to read config variable from env.')
+ } else {
+ logError('Unable to read config variable from env.', e)
+ }
+ }
+ }
+}
+
+export const parseConfig = (configString: string): Object => {
+ try {
+ const parsedConfig = jsonc.parse(configString)
+ logInfo('Successfully parsed configuration.')
+ return parsedConfig
+ } catch (e) {
+ logError('Unable to parse configuration.', e)
+ }
+}
+
+export const getConfigSchema = (): Definition => {
+ const basePath = resolve(join(__dirname, '../..'))
+ const config = resolve(join(basePath, 'src/common/config.ts'))
+ const tsconfig = require(resolve(join(basePath, 'tsconfig.json')))
+ const types = resolve(join(basePath, 'node_modules/@types'))
+
+ // Generate JSON schema at runtime for Config interface above
+ const compilerOptions: CompilerOptions = {
+ ...tsconfig.compilerOptions,
+ typeRoots: [types],
+ baseUrl: basePath
+ }
+
+ const settings: PartialArgs = {
+ required: true,
+ defaultProps: true,
+ noExtraProps: true
+ }
+
+ try {
+ const program = getProgramFromFiles([config], compilerOptions, basePath)
+ const configSchema = generateSchema(program, 'Config', settings)
+
+ return configSchema
+ } catch (e) {
+ logError('Could not generate config schema.', e)
+ }
+}
+
+export const validateConfig = (parsedConfig: Object): Config => {
+ const configSchema = getConfigSchema()
+
+ // Validate parsed configuration object against generated JSON schema
+ try {
+ const validator = new Ajv()
+ const valid = validator.validate(configSchema, parsedConfig)
+
+ if (!valid) {
+ logError('Unable to validate configuration.', validator.errors)
+ }
+ } catch (e) {
+ logError('Unable to validate configuration.', e)
+ }
+
+ const validatedConfig = parsedConfig as Config
+ logInfo('Successfully validated configuration.')
+ return validatedConfig
+}
+
+export const getConfig = (): Config => {
+ const configSource = getConfigSource()
+ const configString = readConfig(configSource)
+ const parsedConfig = parseConfig(configString)
+ const validatedConfig = validateConfig(parsedConfig)
+ return validatedConfig
+}
+
+export const writeConfig = (source: ConfigSource, config: Config): void => {
+ if (source.type === 'file') {
+ try {
+ fs.writeFileSync(source.path, jsonc.stringify(config, null, 2))
+ logInfo('Successfully wrote configuration file.')
+ } catch (e) {
+ logError('Unable to write configuration file.', e)
+ }
+ }
+ if (source.type === 'environment') {
+ logError(
+ 'Node does not have permissions to modify global environment variables. Please use file-based configuration to make changes.'
+ )
+ }
+}
+
+type ConfigTransformer = (oldConfig: Config) => Config
+
+export const updateConfig = (configTransformer: ConfigTransformer, initialize?: boolean): Config => {
+ let newConfig: Config
+ const configSource = getConfigSource()
+
+ if (initialize) {
+ newConfig = configTransformer(DEFAULT_CONFIG)
+ } else {
+ const configString = readConfig(configSource)
+ const oldConfig = parseConfig(configString) as Config
+ newConfig = configTransformer(oldConfig)
+ }
+
+ const validatedConfig = validateConfig(newConfig)
+ writeConfig(configSource, validatedConfig)
+ return validatedConfig
+}
diff --git a/src/common/logging.ts b/src/common/logging.ts
new file mode 100644
index 00000000..e1781b6f
--- /dev/null
+++ b/src/common/logging.ts
@@ -0,0 +1,121 @@
+const chalk = require('chalk')
+import { argv } from 'yargs'
+import { inspect } from 'util'
+import * as os from 'os'
+
+export enum LogLevel {
+ Info = 'info',
+ Warn = 'warn',
+ Error = 'error'
+}
+
+export interface LogRequest {
+ level: LogLevel
+ message: string
+ data?: any
+}
+
+const sanitize = (data: any) => {
+ const blacklist = [
+ 'account.?id',
+ 'account',
+ 'client.?id',
+ 'client.?secret',
+ 'private.?key',
+ 'private.?token',
+ 'public.?key',
+ 'public.?token',
+ 'refresh.?token',
+ 'secret',
+ 'spreadsheet.?id',
+ 'spreadsheet',
+ 'token'
+ ]
+
+ if (typeof data === 'string') {
+ blacklist.forEach(term => {
+ data = data.replace(RegExp(`(${term}).?(.*)`, 'gi'), `$1=`)
+ })
+ return data
+ } else if (typeof data === 'boolean') {
+ return data
+ } else if (typeof data === 'number') {
+ return data
+ } else if (Array.isArray(data)) {
+ return data.map(sanitize)
+ } else if (typeof data === 'object') {
+ let sanitized = {}
+ for (const key in data) {
+ sanitized[sanitize(key) as string] = sanitize(data[key])
+ }
+ return sanitized
+ } else {
+ return '[redacted]'
+ }
+}
+
+export const log = (request: LogRequest): void => {
+ if (argv['ci']) {
+ request.message = sanitize(request.message)
+ request.data = sanitize(request.data)
+ }
+
+ const date = chalk.bold(new Date().toISOString())
+ const level = chalk.bold(`[${request.level.toUpperCase()}]`)
+ const text = `${date} ${level} ${request.message}`
+
+ switch (request.level) {
+ case LogLevel.Error:
+ console.error(chalk.red(text))
+ console.error('\n', chalk.red(inspect(request.data, true, 10)), '\n')
+
+ const searchIssuesLink = encodeURI(
+ `https://github.com/kevinschaich/mintable/issues?q=is:issue+${request.message}`
+ )
+ const searchIssuesHelpText = `You can check if anybody else has encountered this issue on GitHub:\n${searchIssuesLink}\n`
+ console.warn(chalk.yellow(searchIssuesHelpText))
+
+ const systemInfo = `arch: ${os.arch()}\nplatform: ${os.platform()}\nos: v${os.release()}\nmintable: v${
+ require('../../package.json').version
+ }\nnode: ${process.version}`
+ const reportIssueBody =
+ '**Steps to Reproduce:**\n\n1.\n2.\n3.\n\n**Error:**\n\n```\n\n```\n\n**System Info:**\n\n```\n' +
+ systemInfo +
+ '\n```'
+ const reportIssueLink = encodeURI(
+ `https://github.com/kevinschaich/mintable/issues/new?title=Error:+${request.message}&body=${reportIssueBody}`
+ )
+ const reportIssueHelpText = `If this is a new issue, please use this link to report it:\n${reportIssueLink}\n`
+ console.warn(chalk.yellow(reportIssueHelpText))
+
+ process.exit(1)
+ case LogLevel.Warn:
+ console.warn(chalk.yellow(text))
+ break
+ case LogLevel.Info:
+ console.info(text)
+ break
+ default:
+ break
+ }
+
+ if (argv['debug']) {
+ try {
+ console.log('\n', inspect(request.data, true, 10), '\n')
+ } catch (e) {
+ console.log('\n', JSON.stringify(request.data, null, 2), '\n')
+ }
+ }
+}
+
+export const logError = (message: string, data?: any): void => {
+ log({ level: LogLevel.Error, message, data })
+}
+
+export const logWarn = (message: string, data?: any): void => {
+ log({ level: LogLevel.Warn, message, data })
+}
+
+export const logInfo = (message: string, data?: any): void => {
+ log({ level: LogLevel.Info, message, data })
+}
diff --git a/src/components/account.jsx b/src/components/account.jsx
deleted file mode 100644
index fd1d2dfa..00000000
--- a/src/components/account.jsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { FiRefreshCw, FiTrash2, FiXCircle, FiCheckCircle } from 'react-icons/fi'
-import { fetch } from './helpers'
-
-const Account = props => {
- let status =
- if (props.details.error) {
- status = (
-
-
- Error
-
- )
- }
-
- const handleOnClickDelete = e => {
- if (confirm(`Are you sure you want to remove the account ${props.details.nickname}?`)) {
- const body = {
- id: `PLAID_TOKEN_${props.details.nickname}`
- }
-
- fetch(`http://${process.env.HOST}:${process.env.PORT}/config`, {
- method: 'DELETE',
- body: JSON.stringify(body),
- headers: {
- 'Content-Type': 'application/json'
- }
- })
- }
- }
-
- const handleOnClickUpdate = e => {
- const body = {
- accountNickname: props.details.nickname
- }
-
- fetch(`http://${process.env.HOST}:${process.env.PORT}/update`, {
- method: 'POST',
- body: JSON.stringify(body),
- headers: {
- 'Content-Type': 'application/json'
- }
- }).then(data => {
- props.handleOnUpdateAccountResponse(data, props.details.nickname)
- })
- }
-
- return (
-