mega-commit
This commit is contained in:
232
spending_summary.md
Normal file
232
spending_summary.md
Normal file
@@ -0,0 +1,232 @@
|
||||
Your bank lets you set up SMS alerts for various triggers; it might even give you the option of receiving periodic spending summaries (mine doesn't!).
|
||||
But what about a daily SMS summary of your spending across *all* your accounts? This is harder to come by, so let's roll our own:
|
||||
|
||||
## Pre-work
|
||||
|
||||
1. [Nab a sandbox account](https://dashboard.plaid.com/signup) from Plaid, and put those credentials into `PLAID_CLIENT_ID`, `PLAID_SECRET`,
|
||||
and `PLAID_PUBLIC_KEY` environment variables. While you're at it,
|
||||
[ask for access](https://dashboard.plaid.com/overview/request-development) the development API (it'll take a few days).
|
||||
For now, though, create an env var `PLAID_ENV` and set it to 'sandbox'.
|
||||
|
||||
```bash
|
||||
export PLAID_CLIENT_ID='somechars1234'
|
||||
export PLAID_PUBLIC_KEY='somemorechars1234'
|
||||
export PLAID_SECRET='somesecretchars1234'
|
||||
export PLAID_ENV='sandbox'
|
||||
```
|
||||
|
||||
1. Clone the [quickstart for Plaid](https://github.com/plaid/quickstart) and reference those environment variables in the
|
||||
Python quickstart's `server.py`.
|
||||
|
||||
```python
|
||||
PLAID_CLIENT_ID = os.getenv('PLAID_CLIENT_ID')
|
||||
PLAID_SECRET = os.getenv('PLAID_SECRET')
|
||||
PLAID_PUBLIC_KEY = os.getenv('PLAID_PUBLIC_KEY')
|
||||
PLAID_ENV = os.getenv('PLAID_ENV')
|
||||
```
|
||||
|
||||
1. Run `server.py` (Python 2 only 😦) and log into Chase with the test credentials ("user_good" and
|
||||
"pass_good" as of 5/24/2017). It prints the access token to your terminal: Grab that and put it into a
|
||||
`CHASE_ACCESS_TOKEN` environment variable. Repeat this for Bank of America and put that access token into `BOFA_ACCESS_TOKEN`.
|
||||
|
||||
1. [Grab your Twilio credentials](https://www.twilio.com/console/account/settings) and
|
||||
[Twilio incoming phone number](https://www.twilio.com/console/phone-numbers/incoming) make sure those are available as
|
||||
environment variables too (see below).
|
||||
|
||||
1. Put your cell phone number in an environment variable as `MY_CELL`.
|
||||
|
||||
```bash
|
||||
export CHASE_ACCESS_TOKEN='access-sandbox-someprettysecretchars1234'
|
||||
export BOFA_ACCESS_TOKEN='access-sandbox-somemoreprettysecretchars1234'
|
||||
export TWILIO_SID='somechars1234'
|
||||
export TWILIO_TOKEN='somesecretchars1234'
|
||||
export MY_TWILIO_NUM='+11111111111'
|
||||
export MY_CELL='+12222222222'
|
||||
```
|
||||
|
||||
1. Install just a couple of dependencies:
|
||||
|
||||
```bash
|
||||
$ pip install python-plaid twilio
|
||||
```
|
||||
|
||||
## Get Some Transactions
|
||||
|
||||
Make yourself a Plaid client instance and grab some transactions from Chase:
|
||||
|
||||
[get_some_transactions_v1.py](https://github.com/zevaverbach/spending_summary/blob/master/get_some_transactions_v1.py)
|
||||
|
||||
```python
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
# twilio.rest has a Client too, so let's avoid a namespace collision
|
||||
from plaid import Client as PlaidClient
|
||||
|
||||
plaid_client = PlaidClient(client_id=os.getenv('PLAID_CLIENT_ID'), secret=os.getenv('PLAID_SECRET'),
|
||||
public_key=os.getenv('PLAID_PUBLIC_KEY'), environment=os.getenv('PLAID_ENV'))
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
return plaid_client.Transactions.get(access_token, start_date, end_date)
|
||||
|
||||
```
|
||||
|
||||
Inspecting the output of `get_some_transactions`, we see that there are multiple accounts, 337 transactions among them, but only
|
||||
100 transactions returned from this API call. Two of these accounts are for savings, so presumably they're only
|
||||
going to have transfers rather than purchases.
|
||||
|
||||
```python
|
||||
>>> from get_some_transactions_v1 import get_some_transactions
|
||||
>>> import os
|
||||
>>> some_transactions = get_some_transactions(os.getenv('CHASE_ACCESS_TOKEN'), '1972-01-01', '2017-05-26'))
|
||||
>>> some_transactions['total_transactions']
|
||||
337
|
||||
>>> from pprint import pprint
|
||||
>>> pprint(some_transactions['accounts'])
|
||||
[{'account_id': 'qwp96Z11b5IBKVMl8XvLSkJXjgj6ZxIXX3o79',
|
||||
'name': 'Plaid Checking',
|
||||
'subtype': 'checking'
|
||||
...},
|
||||
{'account_id': 'Kk9ZL7NN4wSX3lR9evV8f9P4GVGk3BF33QnAM',
|
||||
'name': 'Plaid Saving',
|
||||
'subtype': 'savings'
|
||||
...},
|
||||
{'account_id': 'rEy96MWWgXukrnBW4yVphv7yl3lznosBBzo6n',
|
||||
'name': 'Plaid CD',
|
||||
'subtype': 'cd'
|
||||
...}
|
||||
{'account_id': '9rNKomMMdWTvVL4X9RP6UKb4qEqng1uJJ6nQw',
|
||||
'name': 'Plaid Credit Card',
|
||||
'subtype': 'credit'
|
||||
...}]
|
||||
>>> len(some_transactions['transactions'])
|
||||
100
|
||||
```
|
||||
## Get **The Right** Transactions
|
||||
|
||||
Looking at the transactions themselves, we see that there is a `category` field which sometimes has a list of values,
|
||||
sometimes `None`. Among the categories there are "Transfer", "Credit Card", and "Deposit": These aren't going to be
|
||||
useful in gleaning spending activity, so we'll refactor our `get_some_transactions` function to 1) skip transactions
|
||||
with those categories and 2) skip accounts with a subtype of "savings" or "cd". Let's also 3) make sure to get all
|
||||
available transactions by using pagination and 4) just return transactions.
|
||||
|
||||
```python
|
||||
>>> some_transactions['transactions'].keys()
|
||||
dict_keys(['account_id', 'account_owner', 'amount', 'category', 'category_id', 'date', 'location', 'name', 'payment_meta', 'pending', 'pending_transaction_id', 'transaction_id', 'transaction_type'])
|
||||
>>> {category for transaction in some_transactions['transactions'] if some_transactions['category'] for category in trans['category']}
|
||||
{'Airlines and Aviation Services',
|
||||
'Coffee Shop',
|
||||
'Credit Card',
|
||||
'Deposit',
|
||||
'Fast Food',
|
||||
'Food and Drink',
|
||||
'Payment',
|
||||
'Restaurants',
|
||||
'Transfer',
|
||||
'Travel'}
|
||||
```
|
||||
|
||||
[get_some_transactions_v2.py](https://github.com/zevaverbach/spending_summary/blob/master/get_some_transactions_v2.py)
|
||||
|
||||
```python
|
||||
import math
|
||||
...
|
||||
# https://plaid.com/docs/api/#transactions
|
||||
MAX_TRANSACTIONS_PER_PAGE = 500
|
||||
OMIT_CATEGORIES = ["Transfer", "Credit Card", "Deposit"]
|
||||
|
||||
|
||||
def get_some_transactions(access_token: str, start_date: str, end_date: str) -> List[dict]:
|
||||
account_ids = [account['account_id'] for account in plaid_client.Accounts.get(access_token)['accounts']
|
||||
if account['subtype'] not in ['cd', 'savings']]
|
||||
|
||||
num_available_transactions = plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids)['total_transactions']
|
||||
num_pages = math.ceil(num_available_transactions / MAX_TRANSACTIONS_PER_PAGE)
|
||||
transactions = []
|
||||
|
||||
for page_num in range(num_pages):
|
||||
transactions += [transaction
|
||||
for transaction in plaid_client.Transactions.get(access_token, start_date, end_date,
|
||||
account_ids=account_ids,
|
||||
offset=page_num * MAX_TRANSACTIONS_PER_PAGE,
|
||||
count=MAX_TRANSACTIONS_PER_PAGE)['transactions']
|
||||
if transaction['category'] is None
|
||||
or not any(category in OMIT_CATEGORIES
|
||||
for category in transaction['category'])]
|
||||
|
||||
return transactions
|
||||
```
|
||||
|
||||
Now there are just 265 transactions. Are any of them negative?
|
||||
|
||||
```python
|
||||
>>> pprint([transaction for transaction in some_transactions if transaction['amount'] < 0])
|
||||
[{'amount': -500,
|
||||
'category': ['Travel', 'Airlines and Aviation Services'],
|
||||
'name': 'United Airlines',
|
||||
'transaction_type': 'special',
|
||||
...},
|
||||
...]
|
||||
```
|
||||
|
||||
Okay, that seems legit -- airfare refund, I guess. Let's keep negative items in.
|
||||
|
||||
## Pulling it All Together
|
||||
|
||||
Now let's get all the transactions from yesterday, making sure to pull them from both accounts.
|
||||
|
||||
[get_yesterdays.py](https://github.com/zevaverbach/spending_summary/blob/master/get_yesterdays.py)
|
||||
|
||||
```python
|
||||
...
|
||||
def get_yesterdays_transactions() -> List[dict]:
|
||||
yesterday = ('2017-05-16' if os.getenv('PLAID_ENV') == 'sandbox'
|
||||
else (datetime.date.today() - datetime.timedelta(days=1)).strftime('%Y-%m-%d'))
|
||||
|
||||
transactions = []
|
||||
|
||||
for access_id in [os.getenv('CHASE_ACCESS_TOKEN'), os.getenv('BOFA_ACCESS_TOKEN')]:
|
||||
transactions += get_transactions_from_multiple_accounts(access_id, yesterday, yesterday)
|
||||
|
||||
return transactions
|
||||
```
|
||||
|
||||
As of 5/24/2017, the most recent transactions available in these sandbox accounts are from 5/16/17: Hence, the hardcoded
|
||||
`yesterday` value above.
|
||||
|
||||
Let's send an SMS to ourselves with the total spent yesterday!
|
||||
|
||||
[send_summary.py](https://github.com/zevaverbach/spending_summary/blob/master/send_summary.py)
|
||||
|
||||
```python
|
||||
...
|
||||
from twilio.rest import Client as TwilioClient
|
||||
|
||||
twilio_client = TwilioClient(os.getenv('TWILIO_SID'), os.getenv('TWILIO_TOKEN'))
|
||||
|
||||
|
||||
def send_summary(transactions: List[dict]) -> None:
|
||||
total_spent = sum(transaction['amount'] for transaction in transactions)
|
||||
|
||||
message = f'You spent ${total_spent} yesterday. 💸'
|
||||
|
||||
twilio_client.api.account.messages.create(to=os.getenv('MY_CELL'), from_=os.getenv('MY_TWILIO_NUM'), body=message)
|
||||
|
||||
|
||||
```
|
||||
Voila!
|
||||
|
||||

|
||||
|
||||
## Representing Money as a Float?
|
||||
|
||||
You're gonna have a bad time, 'mkay? If you build anything significant (budget tracker bot?) with the Plaid API,
|
||||
be sure to convert those transaction amounts to Decimals:
|
||||
|
||||
```python
|
||||
import decimal
|
||||
def convert_transaction_amount(transaction_amount):
|
||||
return decimal.Decimal(transaction_amount)
|
||||
```
|
||||
Reference in New Issue
Block a user