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) }