-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 179afaa
Showing
8 changed files
with
509 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Monzo Go Client | ||
|
||
A Go client for interacting with the Monzo API. | ||
|
||
## Installation | ||
|
||
``` | ||
go get -u github.com/tmus/monzo | ||
``` | ||
|
||
## Usage | ||
|
||
> The client API may change at some point, don't rely on this | ||
> for anything serious, and make sure you use version numbers | ||
> in your `go.mod` file. | ||
1. Create a `monzo.Client` and pass it your Monzo access token: | ||
|
||
```go | ||
token, _ := os.LookupEnv("MONZO_TOKEN") | ||
c = monzo.NewClient(token) | ||
``` | ||
|
||
Creating a new client doesn't verify the connection to Monzo, | ||
so you should call the `Ping` method on the new client to ensure | ||
that the client can access Monzo: | ||
|
||
```go | ||
if err := c.Ping(); err != nil { | ||
panic(err) | ||
} | ||
``` | ||
|
||
2. Call the `Accounts` function on the client to return a slice | ||
of accounts associated with the Monzo token. | ||
|
||
```go | ||
accs, _ := c.Accounts() | ||
|
||
for _, acc := range accs { | ||
fmt.Println(acc.ID) | ||
} | ||
``` | ||
|
||
3. You can call the `Account` function on the client to return | ||
a single account. | ||
|
||
```go | ||
acc, _ := c.Account("acc_00000XXXXXXXXXXXXXXXXX") | ||
fmt.Println(acc.ID) | ||
``` | ||
|
||
**More details coming soon.** |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package monzo | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
type Account struct { | ||
ID string | ||
Closed bool | ||
Created string | ||
Description string | ||
Type AccountType | ||
Currency AccountCurrency | ||
Country string `json:"country_code"` | ||
AccountNumber string `json:"account_number"` | ||
SortCode string `json:"sort_code"` | ||
|
||
// The monzo.Client is embedded here to enable a fluent API. | ||
client *Client | ||
} | ||
|
||
// AccountCurrency is a label for the currency that the account | ||
// represents. Currently only GBP? | ||
type AccountCurrency string | ||
|
||
// AccountType is the way that Monzo identifies accounts internally. | ||
type AccountType string | ||
|
||
const ( | ||
// PrepaidAccount is for accounts that were created before | ||
// the UKRetailAccount existed and can no longer be opened. | ||
PrepaidAccount AccountType = "uk_prepaid" | ||
// UKRetailAccount is a Current Account. | ||
UKRetailAccount AccountType = "uk_retail" | ||
// UKRetailJointAccount is a Current Account shared by two | ||
// Monzo users. | ||
UKRetailJointAccount AccountType = "uk_retail_joint" | ||
|
||
// CurrencyGBP is Pound Sterling. | ||
CurrencyGBP AccountCurrency = "GBP" | ||
) | ||
|
||
// Balance returns the current balance for the Account that | ||
// it is called on. | ||
func (a Account) Balance() (Balance, error) { | ||
req, err := a.client.NewRequest(http.MethodGet, "balance", nil) | ||
if err != nil { | ||
return Balance{}, err | ||
} | ||
|
||
q := req.URL.Query() | ||
q.Add("account_id", a.ID) | ||
req.URL.RawQuery = q.Encode() | ||
|
||
resp, _ := a.client.Do(req) | ||
|
||
b := new(bytes.Buffer) | ||
b.ReadFrom(resp.Body) | ||
str := b.String() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return Balance{}, fmt.Errorf("failed to fetch balance: %s", str) | ||
} | ||
|
||
var bal Balance | ||
if err := json.Unmarshal(b.Bytes(), &bal); err != nil { | ||
return Balance{}, err | ||
} | ||
|
||
return bal, nil | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package monzo | ||
|
||
import "fmt" | ||
|
||
// Balance returns the data from the Monzo API's /balance endpoint. | ||
// All values are returned in pence. | ||
type Balance struct { | ||
Balance int | ||
Total int `json:"total_balance"` | ||
WithSavings int `json:"balance_including_flexible_savings"` | ||
Currency string | ||
} | ||
|
||
func (b Balance) String() string { | ||
return fmt.Sprintf( | ||
"%s %.2f (Total: %s %.2f)", | ||
b.Currency, | ||
float64(b.Balance/100), | ||
b.Currency, | ||
float64(b.Total/100), | ||
) | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package monzo | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"net/http" | ||
) | ||
|
||
// Deposit represents a deposit that is ready to be made against | ||
// Monzo. It is read-only. There are no side effects if the | ||
// deposit is not ran. | ||
type Deposit struct { | ||
Request *http.Request | ||
Client *http.Client | ||
} | ||
|
||
// Run executes the deposit against the Monzo API. An error is | ||
// only returned if the deposit fails to run. If the deposit | ||
// has already ran against the account, it is not ran again | ||
// and an error is not returned. | ||
func (d Deposit) Run() error { | ||
resp, _ := d.Client.Do(d.Request) | ||
|
||
b := new(bytes.Buffer) | ||
b.ReadFrom(resp.Body) | ||
str := b.String() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("failed to fetch accounts: %s", str) | ||
} | ||
|
||
return nil | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/tmus/monzo | ||
|
||
go 1.12 |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
package monzo | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"math/rand" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const ( | ||
// APIBase is the root of the Monzo API. | ||
APIBase string = "https://api.monzo.com" | ||
) | ||
|
||
// Client is the way to interact with the Monzo API. | ||
type Client struct { | ||
Token string | ||
|
||
http.Client | ||
} | ||
|
||
// NewClient uses the passed token to create a new Monzo Client. | ||
// No validation is done on this method so users should call | ||
// the Ping method after creating a client to ensure that | ||
// the new connection has been created successsfully. | ||
func NewClient(token string) *Client { | ||
return &Client{ | ||
Token: token, | ||
} | ||
} | ||
|
||
// Ping attempts to connect to the Monzo API using the given | ||
// client. | ||
// | ||
// No "ping" endpoint currently exists at Monzo, so for now | ||
// this simply queries the /accounts endpoint. | ||
func (c *Client) Ping() error { | ||
req, err := c.NewRequest(http.MethodGet, "accounts", nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
resp, _ := c.Do(req) | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
b := new(bytes.Buffer) | ||
b.ReadFrom(resp.Body) | ||
str := b.String() | ||
return fmt.Errorf("error pinging Monzo API. JSON response: %s", str) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (c *Client) NewRequest(method string, endpoint string, body io.Reader) (*http.Request, error) { | ||
req, err := http.NewRequest(method, APIBase+"/"+endpoint, body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req.Header.Add("Authorization", "Bearer "+c.Token) | ||
return req, nil | ||
} | ||
|
||
func (c *Client) resourceRequest(resource string) (*http.Request, error) { | ||
req, err := c.NewRequest(http.MethodGet, resource, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return req, nil | ||
} | ||
|
||
// Account returns a single Account from the Monzo API. If the | ||
// account is not found (does not exist), an error is returned. | ||
func (c *Client) Account(id string) (Account, error) { | ||
accs, err := c.accounts() | ||
if err != nil { | ||
return Account{}, err | ||
} | ||
|
||
// Monzo doesn't have the capability to retrieve a single | ||
// account, so we need to get all the users accounts | ||
// and filter them down using the provided id. | ||
for _, acc := range accs { | ||
if acc.ID == id { | ||
return acc, nil | ||
} | ||
} | ||
|
||
return Account{}, fmt.Errorf("no account found with ID %s", id) | ||
} | ||
|
||
// Accounts returns a slice of Account structs, one for each of | ||
// the Monzo accounts associated with the authentication. | ||
func (c *Client) Accounts() ([]Account, error) { | ||
return c.accounts() | ||
} | ||
|
||
func (c *Client) accounts() ([]Account, error) { | ||
req, err := c.resourceRequest("accounts") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
resp, _ := c.Do(req) | ||
|
||
b := new(bytes.Buffer) | ||
b.ReadFrom(resp.Body) | ||
str := b.String() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return nil, fmt.Errorf("failed to fetch accounts: %s", str) | ||
} | ||
|
||
bytes := b.Bytes() | ||
var accounts []Account | ||
if err := unwrapJSON(bytes, "accounts", &accounts); err != nil { | ||
return nil, err | ||
} | ||
|
||
var accs []Account | ||
for _, acc := range accounts { | ||
// The API still returns the Monzo beta prepaid accounts. | ||
// These can't be actioned meaningfully, so they are | ||
// removed from the slice if they exist. | ||
if acc.Type != PrepaidAccount { | ||
// To provide a fluent API for the account, it needs | ||
// to know how to talk to Monzo. The monzo.Client | ||
// is embedded in the Account struct so that | ||
// calls can be passed to it. | ||
acc.client = c | ||
accs = append(accs, acc) | ||
} | ||
} | ||
|
||
return accs, nil | ||
} | ||
|
||
// Deposit creates a new Deposit struct. Monzo uses a 'dedupe_id' | ||
// to ensure that the request is idempotent, so the deposit is | ||
// not ran when it is created. To action the deposit, call | ||
// the `Run` method on it. | ||
func (a Account) Deposit(p Pot, amt int) (*Deposit, error) { | ||
endpoint := "/pots/" + p.ID + "/deposit" | ||
|
||
data := url.Values{} | ||
data.Add("source_account_id", a.ID) | ||
data.Add("amount", strconv.Itoa(amt)) | ||
|
||
src := rand.NewSource(time.Now().UnixNano()) | ||
r := rand.New(src) | ||
|
||
data.Add("dedupe_id", strconv.FormatFloat(r.Float64(), 'f', 6, 64)) | ||
|
||
req, err := a.client.NewRequest(http.MethodPut, endpoint, strings.NewReader(data.Encode())) | ||
if err != nil { | ||
return &Deposit{}, err | ||
} | ||
|
||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
|
||
return &Deposit{req, &a.client.Client}, nil | ||
} | ||
|
||
// Withdraw creates a new Withdrawal struct. Monzo uses a 'dedupe_id' | ||
// to ensure that the request is idempotent, so the withdrawal is | ||
// not ran when it is created. To action the withdrawal, call | ||
// the `Run` method on it. | ||
func (a Account) Withdraw(p Pot, amt int) (*Withdrawal, error) { | ||
endpoint := "/pots/" + p.ID + "/withdraw" | ||
data := url.Values{} | ||
data.Add("destination_account_id", a.ID) | ||
data.Add("amount", strconv.Itoa(amt)) | ||
|
||
src := rand.NewSource(time.Now().UnixNano()) | ||
r := rand.New(src) | ||
|
||
data.Add("dedupe_id", strconv.FormatFloat(r.Float64(), 'f', 6, 64)) | ||
|
||
req, err := a.client.NewRequest(http.MethodPut, endpoint, strings.NewReader(data.Encode())) | ||
if err != nil { | ||
return &Withdrawal{}, err | ||
} | ||
|
||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") | ||
|
||
return &Withdrawal{req, &a.client.Client}, nil | ||
} |
Oops, something went wrong.