Created
September 11, 2019 11:50
-
-
Save kieranja/9b38c3d8daae1829cdcfe5cd89e639b0 to your computer and use it in GitHub Desktop.
Export CSV statement from Monzo excluding Pot Transactions (total balance)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/csv" | |
"encoding/json" | |
"fmt" | |
"github.com/davecgh/go-spew/spew" | |
"github.com/manifoldco/promptui" | |
"io" | |
"log" | |
"net/http" | |
"net/url" | |
"os" | |
"strings" | |
"time" | |
) | |
// global | |
var ACCESS_TOKEN = "" | |
type Account struct { | |
Id string `json:"id"` | |
Description string `json:"description"` | |
Created string `json:"created"` | |
} | |
type Accounts struct { | |
Accounts []Account `json:"accounts"` | |
} | |
type Transaction struct { | |
AccountBalance int64 `json:"account_balance"` | |
Amount int64 `json:"amount"` | |
Created time.Time `json:"created"` | |
Description string `json:"description"` | |
Currency string `json:"currency"` | |
Id string `json:"id"` | |
} | |
type Transactions struct { | |
Transactions []Transaction `json:"transactions"` | |
} | |
type Balance struct { | |
TotalBalance int64 `json:"total_balance"` | |
Balance int64 `json:"balance"` | |
} | |
// Get all accounts. | |
func getAccounts(account_type string) Accounts { | |
params := url.Values{} | |
// params.Add("account_type", account_type) | |
body, _ := makeReq("accounts", params) | |
var accounts Accounts | |
json.NewDecoder(body).Decode(&accounts) | |
return accounts | |
} | |
func getBalance(account_id string) Balance { | |
params := url.Values{} | |
params.Add("account_id", account_id) | |
body, _ := makeReq("balance", params) | |
var balanceTest Balance | |
json.NewDecoder(body).Decode(&balanceTest) | |
spew.Dump(balanceTest) | |
return balanceTest | |
} | |
func makeReq(endpoint string, queryString url.Values) (io.ReadCloser, error) { | |
client := &http.Client{} | |
url := "https://api.monzo.com/" | |
url += endpoint | |
url += "?" + queryString.Encode() | |
spew.Dump(url) | |
request, err := http.NewRequest("GET", url, nil) | |
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", ACCESS_TOKEN)) | |
if err != nil { | |
log.Fatalln(err) | |
// break | |
} | |
resp, err := client.Do(request) | |
if err != nil { | |
log.Fatalln(err) | |
} | |
return resp.Body, nil | |
} | |
func getTransactions(account Account, fromDate time.Time) (Transactions, time.Time, time.Time) { | |
params := url.Values{} | |
params.Add("account_id", account.Id) | |
params.Add("since", fromDate.Format(time.RFC3339)) | |
body, _ := makeReq("transactions", params) | |
var transactions Transactions | |
json.NewDecoder(body).Decode(&transactions) | |
// Flip transactions to handle Monzo's API shortcomings (account_balance does not have a value) | |
// So we'll need to flip all transactions to replay transactions. I'll do this by taking todays "total balance" | |
// and for each transaction subtract the transaction amount. The downside of this approach is that if we need Jan 19s statements, | |
// we'll need to pull all transactions from Jan-Now() and then flip them. :/ | |
for i, j := 0, len(transactions.Transactions)-1; i < j; i, j = i+1, j-1 { | |
transactions.Transactions[i], transactions.Transactions[j] = transactions.Transactions[j], transactions.Transactions[i] | |
} | |
var newTransactions []Transaction | |
balanceObject := getBalance(account.Id) | |
runningBalance := balanceObject.TotalBalance | |
// Now iterate through reorder and add a balance field. | |
for _, element := range transactions.Transactions { | |
// Ignore pot transactions. | |
if strings.Contains(element.Description, "pot_") { | |
continue | |
} | |
element.AccountBalance = runningBalance | |
newTransactions = append(newTransactions, element) | |
runningBalance -= element.Amount | |
} | |
transactions.Transactions = newTransactions | |
endOfMonth := EndOfMonth(fromDate) | |
// Last piece of logic to filter. | |
filtered := make([]Transaction, 0) | |
for _, element := range transactions.Transactions { | |
if element.Created.Before(endOfMonth) { | |
filtered = append(filtered, element) | |
} | |
} | |
transactions.Transactions = filtered | |
// If we're not at end of month set statement to last transaction date | |
today := time.Now() | |
if endOfMonth.After(today) { | |
endOfMonth = transactions.Transactions[0].Created | |
} | |
return transactions, fromDate, endOfMonth | |
} | |
func main() { | |
token := os.Getenv("MONZO_ACCESS_TOKEN") | |
if token == "" { | |
fmt.Println("You need to set an access token") | |
// os.exit(1) | |
os.Exit(3) | |
} | |
// global variable - lazy | |
ACCESS_TOKEN = token | |
// Get accounts - the "type" param isnt actually used. | |
accounts := getAccounts("uk_business") | |
// Ask for user to pick account | |
prompt := promptui.Select{ | |
Label: "Select Account", | |
Items: accounts.Accounts, | |
} | |
idx, result, err := prompt.Run() | |
if err != nil { | |
fmt.Printf("Prompt failed %v\n", err) | |
return | |
} | |
fmt.Printf("You choose %q\n", result) | |
selectedAccount := accounts.Accounts[idx] | |
dates := getStmtDates(selectedAccount) | |
prompt = promptui.Select{ | |
Label: "Select Statement Date", | |
Items: dates, | |
} | |
idx, result, err = prompt.Run() | |
if err != nil { | |
fmt.Printf("Prompt failed %v\n", err) | |
return | |
} | |
fmt.Printf("You choose %q\n", result) | |
// No error handling :D | |
startDate, err := time.Parse("2 Jan 2006", result) | |
if err != nil { | |
fmt.Printf("Picked an invalid date. Not sure how.\n", err) | |
return | |
} | |
// get transactions | |
trans, transFrom, _ := getTransactions(selectedAccount, startDate) | |
filename := "Monzo_Bank_Statement_" + strings.Replace(selectedAccount.Description, " ", "-", -1) + "_" + transFrom.Format("2006_01_02") | |
// Bloody finally! | |
generateCSV(trans, filename) | |
dir, err := os.Getwd() | |
if err != nil { | |
log.Fatal(err) | |
} | |
fmt.Println(dir) | |
fmt.Printf("CSV generated and put in same dir as this executable: " + dir + "/" + filename + ".csv\n") | |
} | |
// Function to generate a list of "statement dates" from the users account creation. | |
// ie if the user created their account 10 months ago this will throw out 10 date strings in a list. | |
func getStmtDates(selectedAct Account) []string { | |
createdDate, err := time.Parse(time.RFC3339, selectedAct.Created) | |
if err != nil { | |
// insert some error handling here | |
} | |
from := BeginningOfMonth(time.Now()) | |
var dates []string | |
// Add this month! | |
// dates = append(dates, createdDate.String()) | |
// start from today and go back | |
for from.After(createdDate) { | |
dates = append(dates, from.Format("2 Jan 2006")) | |
// remove a month | |
from = from.AddDate(0, -1, 0) | |
} | |
// Lazy - let's make sure we offer all statement periods. | |
dates = append(dates, createdDate.Format("2 Jan 2006")) | |
return dates | |
} | |
// Simple helper | |
func generateCSV(transactions Transactions, filename string) { | |
file, err := os.Create("./" + filename + ".csv") | |
if err != nil { | |
} | |
defer file.Close() | |
writer := csv.NewWriter(file) | |
defer writer.Flush() | |
for _, element := range transactions.Transactions { | |
// CSV row. | |
row := []string{element.Created.Format("02/01/2006 15:04:05"), element.Description, asCurrency(element.Amount), asCurrency(element.AccountBalance)} | |
writer.Write(row) | |
} | |
} | |
func asCurrency(value int64) string { | |
x := float64(value) | |
x = x / 100 | |
return fmt.Sprintf("%.2f", x) | |
} | |
func BeginningOfMonth(t time.Time) time.Time { | |
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()) | |
} | |
func EndOfMonth(t time.Time) time.Time { | |
return BeginningOfMonth(t).AddDate(0, 1, 0).Add(-time.Second) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment