Skip to content

Instantly share code, notes, and snippets.

@kieranja
Created September 11, 2019 11:50
Show Gist options
  • Save kieranja/9b38c3d8daae1829cdcfe5cd89e639b0 to your computer and use it in GitHub Desktop.
Save kieranja/9b38c3d8daae1829cdcfe5cd89e639b0 to your computer and use it in GitHub Desktop.
Export CSV statement from Monzo excluding Pot Transactions (total balance)
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