Merge "Initial cut at importing financial transactions using Plaid.com. No Plaid-specific UI in this first pass; just import/data model only."

This commit is contained in:
Mathieu Lonjaret 2017-03-26 00:24:48 +00:00 committed by Gerrit Code Review
commit e0107f74bc
22 changed files with 1809 additions and 0 deletions

View File

@ -361,6 +361,9 @@
"picasa": {
"clientSecret": ["_env", "${CAMLI_PICASA_API_KEY}", ""]
},
"plaid": {
"clientSecret": ["_env", "${CAMLI_PLAID_API_KEY}", ""]
},
"twitter": {
"clientSecret": ["_env", "${CAMLI_TWITTER_API_KEY}", ""]
}

View File

@ -73,6 +73,7 @@ type serverCmd struct {
flickrAPIKey string
foursquareAPIKey string
picasaAPIKey string
plaidAPIKey string
twitterAPIKey string
extraArgs string // passed to camlistored
// end of flag vars
@ -117,6 +118,7 @@ func init() {
flags.StringVar(&cmd.flickrAPIKey, "flickrapikey", "", "The key and secret to use with the Flickr importer. Formatted as '<key>:<secret>'.")
flags.StringVar(&cmd.foursquareAPIKey, "foursquareapikey", "", "The key and secret to use with the Foursquare importer. Formatted as '<clientID>:<clientSecret>'.")
flags.StringVar(&cmd.picasaAPIKey, "picasakey", "", "The username and password to use with the Picasa importer. Formatted as '<username>:<password>'.")
flags.StringVar(&cmd.plaidAPIKey, "plaidkey", "", "The client_id and secret to use with the Plaid importer. Formatted as '<client_id>:<secret>'.")
flags.StringVar(&cmd.twitterAPIKey, "twitterapikey", "", "The key and secret to use with the Twitter importer. Formatted as '<APIkey>:<APIsecret>'.")
flags.StringVar(&cmd.root, "root", "", "A directory to store data in. Defaults to a location in the OS temp directory.")
flags.StringVar(&cmd.extraArgs, "extraargs", "",
@ -315,6 +317,10 @@ func (c *serverCmd) setEnvVars() error {
setenv("CAMLI_PICASA_ENABLED", "true")
setenv("CAMLI_PICASA_API_KEY", c.picasaAPIKey)
}
if c.plaidAPIKey != "" {
setenv("CAMLI_PLAID_ENABLED", "true")
setenv("CAMLI_PLAID_API_KEY", c.plaidAPIKey)
}
if c.twitterAPIKey != "" {
setenv("CAMLI_TWITTER_ENABLED", "true")
setenv("CAMLI_TWITTER_API_KEY", c.twitterAPIKey)

View File

@ -24,5 +24,6 @@ import (
_ "camlistore.org/pkg/importer/foursquare"
_ "camlistore.org/pkg/importer/picasa"
_ "camlistore.org/pkg/importer/pinboard"
_ "camlistore.org/pkg/importer/plaid"
_ "camlistore.org/pkg/importer/twitter"
)

17
pkg/importer/plaid/README Normal file
View File

@ -0,0 +1,17 @@
Plaid Importer
==============
Plaid (plaid.com) is a service that allows users to connect to financial
institutions (namely credit card providers) and retreive financial
transactions in a uniform format.
To use:
1) Sign up for a Plaid developer account: https://dashboard.plaid.com/signup
2) Start the devcam server with plaidapikey flag:
$ devcam server -plaidapikey=<client_id>:<secret>
3) Navigate to http://<server>/importer/plaid and click "Add new account"
4) Enter username and password for your financial institution's account, and
select the institution type (eg, Amex). (Unfortunately, OAuth is not
available)
5) Watch your transactions roll on in

View File

@ -0,0 +1,65 @@
/*
Copyright 2017 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plaid
type Institution int
const (
AMEX = iota
BBT
BOFA
CAPONE
SCHWAB
CHASE
CITI
FIDELITY
NFCU
PNC
SUNTRUST
TD
US
USAA
WELLS
)
type InstitutionNames struct {
DisplayName string
CodeName string
}
type InstitutionNameMap map[Institution]InstitutionNames
var supportedInstitutions InstitutionNameMap
func init() {
supportedInstitutions = InstitutionNameMap{
AMEX: {"Amex", "amex"},
BBT: {"BB&T", "bbt"},
BOFA: {"Bank of America", "bofa"},
CAPONE: {"Capital One", "capone"},
CITI: {"Citi", "citi"},
SCHWAB: {"Chales Schwab", "schwab"},
CHASE: {"Chase", "chase"},
FIDELITY: {"Fidelity", "fidelity"},
NFCU: {"Navy FCU", "nfcu"},
PNC: {"PNC", "pnc"},
SUNTRUST: {"Suntrust", "suntrust"},
TD: {"TD Bank", "td"},
US: {"US Bank", "us"},
USAA: {"USAA", "usaa"},
WELLS: {"Wells Fargo", "wells"},
}
}

246
pkg/importer/plaid/plaid.go Normal file
View File

@ -0,0 +1,246 @@
/*
Copyright 2017 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package plaid implements an importer for financial transactions from plaid.com
package plaid // import "camlistore.org/pkg/importer/plaid"
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"time"
"camlistore.org/pkg/blob"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/importer"
"camlistore.org/pkg/schema"
"camlistore.org/pkg/schema/nodeattr"
"github.com/plaid/plaid-go/plaid"
)
func init() {
importer.Register("plaid", &imp{})
}
type imp struct{}
func (*imp) SupportsIncremental() bool {
return true
}
func (*imp) NeedsAPIKey() bool {
return true
}
const (
acctAttrToken = "plaidAccountToken"
acctAttrUsername = "username"
acctInstitution = "institutionType"
plaidTransactionTimeFormat = "2006-01-02"
plaidTransactionNodeType = "plaid.io:transaction"
plaidLastTransaction = "lastTransactionSyncDate"
)
func (*imp) IsAccountReady(acct *importer.Object) (ready bool, err error) {
return acct.Attr(acctAttrToken) != "" && acct.Attr(acctAttrUsername) != "", nil
}
func (*imp) SummarizeAccount(acct *importer.Object) string {
return fmt.Sprintf("%s (%s)", acct.Attr(acctAttrUsername), acct.Attr(acctInstitution))
}
func (*imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error {
args := struct {
Ctx *importer.SetupContext
Inst InstitutionNameMap
}{
ctx,
supportedInstitutions,
}
return tmpl.ExecuteTemplate(w, "serveSetup", args)
}
var tmpl = template.Must(template.New("root").Parse(`
{{define "serveSetup"}}
<h1>Configuring Bank Account</h1>
<p>Enter your username/password credentials for your bank/card account and select the institution type.
<form method="get" action="{{.Ctx.CallbackURL}}">
<input type="hidden" name="acct" value="{{.Ctx.AccountNode.PermanodeRef}}">
<table border=0 cellpadding=3>
<tr><td align=right>Username</td><td><input name="username" size=50></td></tr>
<tr><td align=right>Password</td><td><input name="password" size=50 type="password"></td></tr>
<tr><td>Institution</td><td><select name="institution">
{{range .Inst}}
<option value="{{.CodeName}}">{{.DisplayName}}</option>
{{end}}
</select></td></tr>
<tr><td align=right></td><td align=right><input type="submit" value="Add"></td></tr>
</table>
</form>
{{end}}
`))
var _ importer.ImporterSetupHTMLer = (*imp)(nil)
func (im *imp) AccountSetupHTML(host *importer.Host) string {
return fmt.Sprintf(`
<h1>Configuring Plaid</h1>
<p>Signup for a developer account on <a href='https://dashboard.plaid.com/signup'>Plaid dashboard</a>
<p>After following signup steps and verifying your email, get your developer credentials
(under "Send your first request"), and copy your client ID and secret above.
<p>
`)
}
func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) {
username := r.FormValue("username")
password := r.FormValue("password")
if username == "" || password == "" {
http.Error(w, "Username and password are both required", 400)
return
}
institution := r.FormValue("institution")
clientID, secret, err := ctx.Credentials()
if err != nil {
httputil.ServeError(w, r, fmt.Errorf("Credentials error: %v", err))
return
}
client := plaid.NewClient(clientID, secret, plaid.Tartan)
res, _, err := client.ConnectAddUser(username, password, "", institution, nil)
if err != nil {
httputil.ServeError(w, r, fmt.Errorf("ConnectAddUser error: %v", err))
return
}
if err := ctx.AccountNode.SetAttrs(
"title", fmt.Sprintf("%s account: %s", institution, username),
acctAttrUsername, username,
acctAttrToken, res.AccessToken,
acctInstitution, institution,
); err != nil {
httputil.ServeError(w, r, fmt.Errorf("Error setting attributes: %v", err))
return
}
http.Redirect(w, r, ctx.AccountURL(), http.StatusFound)
}
func (im *imp) Run(ctx *importer.RunContext) (err error) {
log.Printf("Running plaid importer.")
defer func() {
log.Printf("plaid importer returned: %v", err)
}()
clientID, secret, err := ctx.Credentials()
if err != nil {
return err
}
var opt plaid.ConnectGetOptions
if start := ctx.AccountNode().Attr(plaidLastTransaction); start != "" {
opt.GTE = start
}
client := plaid.NewClient(clientID, secret, plaid.Tartan)
resp, _, err := client.ConnectGet(ctx.AccountNode().Attr(acctAttrToken), &opt)
if err != nil {
fmt.Errorf("ConnectGet: %s\n", err)
return
}
var latestTrans string
for _, t := range resp.Transactions {
tdate, err := im.importTransaction(ctx, &t)
if err != nil {
return err
} else if tdate > latestTrans {
latestTrans = tdate
ctx.AccountNode().SetAttr(plaidLastTransaction, latestTrans)
}
}
return nil
}
func (im *imp) importTransaction(ctx *importer.RunContext, t *plaid.Transaction) (string, error) {
itemNode, err := ctx.RootNode().ChildPathObject(t.ID)
if err != nil {
return "", err
}
transJSON, err := json.Marshal(t)
if err != nil {
return "", err
}
fileRef, err := schema.WriteFileFromReader(ctx.Host.Target(), "", bytes.NewBuffer(transJSON))
if err != nil {
return "", err
}
transactionTime, err := time.Parse(plaidTransactionTimeFormat, t.Date)
if err != nil {
return "", err
}
if err := itemNode.SetAttrs(
nodeattr.Type, plaidTransactionNodeType,
nodeattr.DateCreated, schema.RFC3339FromTime(transactionTime),
"transactionId", t.ID,
"vendor", t.Name,
"amount", fmt.Sprintf("%f", t.Amount),
"currency", "USD",
"categoryId", t.CategoryID,
nodeattr.Title, t.Name,
nodeattr.CamliContent, fileRef.String(),
); err != nil {
return "", err
}
// if the transaction includes location information (rare), use the supplied
// lat/long. Partial address data (eg, the US state) without corresponding lat/long
// is also sometimes returned; no attempt is made to geocode that info currently.
if t.Meta.Location.Coordinates.Lat != 0 && t.Meta.Location.Coordinates.Lon != 0 {
if err := itemNode.SetAttrs(
"latitude", fmt.Sprintf("%f", t.Meta.Location.Coordinates.Lat),
"longitude", fmt.Sprintf("%f", t.Meta.Location.Coordinates.Lon),
); err != nil {
return "", err
}
}
return t.Date, nil
}
func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
httputil.BadRequestError(w, "Unexpected path: %s", r.URL.Path)
}
func (im *imp) CallbackRequestAccount(r *http.Request) (blob.Ref, error) {
return importer.OAuth1{}.CallbackRequestAccount(r)
}
func (im *imp) CallbackURLParameters(acctRef blob.Ref) url.Values {
return importer.OAuth1{}.CallbackURLParameters(acctRef)
}

1
vendor/github.com/plaid/plaid-go/.gitignore generated vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

21
vendor/github.com/plaid/plaid-go/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2015-2016 Plaid Technologies, Inc. <support@plaid.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

116
vendor/github.com/plaid/plaid-go/README.md generated vendored Normal file
View File

@ -0,0 +1,116 @@
# plaid-go
plaid-go is a Go client implementation of the [Plaid API](https://plaid.com/docs).
Install via `go get github.com/plaid/plaid-go`.
**Documentation:** [![GoDoc](https://godoc.org/github.com/plaid/plaid-go?status.svg)](https://godoc.org/github.com/plaid/plaid-go/plaid)
TODO:
- Complete README
- Complete testing
- Add CI
## Examples
### Adding an Auth user
```go
client := plaid.NewClient("test_id", "test_secret", plaid.Tartan)
// POST /auth
postRes, mfaRes, err := client.AuthAddUser("plaid_test", "plaid_good", "", "bofa", nil)
if err != nil {
fmt.Println(err)
} else if mfaRes != nil {
// Need to switch on different MFA types. See https://plaid.com/docs/api/#auth-mfa.
switch mfaRes.Type {
case "device":
fmt.Println("--Device MFA--")
fmt.Println("Message:", mfaRes.Device.Message)
case "list":
fmt.Println("--List MFA--")
fmt.Println("Mask:", mfaRes.List[0].Mask, "\nType:", mfaRes.List[0].Type)
case "questions":
fmt.Println("--Questions MFA--")
fmt.Println("Question:", mfaRes.Questions[0].Question)
case "selections":
fmt.Println("--Selections MFA--")
fmt.Println("Question:", mfaRes.Selections[1].Question)
fmt.Println("Answers:", mfaRes.Selections[1].Answers)
}
postRes2, mfaRes2, err := client.AuthStepSendMethod(mfaRes.AccessToken, "type", "email")
if err != nil {
fmt.Println("Error submitting send_method", err)
}
fmt.Println(mfaRes2, postRes2)
postRes2, mfaRes2, err = client.AuthStep(mfaRes.AccessToken, "tomato")
if err != nil {
fmt.Println("Error submitting mfa", err)
} else {
fmt.Println(mfaRes2, postRes2)
}
} else {
fmt.Println(postRes.Accounts)
fmt.Println("Auth Get")
fmt.Println(client.AuthGet("test_bofa"))
fmt.Println("Auth DELETE")
fmt.Println(client.AuthDelete("test_bofa"))
}
```
### Plaid Link Exchange Token Process
Exchange a [Plaid Link][1] `public_token` for an API `access_token`:
```go
client := plaid.NewClient("test_id", "test_secret", plaid.Tartan)
// POST /exchange_token
postRes, err := client.ExchangeToken(public_token)
if err != nil {
fmt.Println(err)
} else {
// Use the returned Plaid API access_token to retrieve
// account information.
fmt.Println(postRes.AccessToken)
fmt.Println("Auth Get")
fmt.Println(client.AuthGet(postRes.AccessToken))
}
```
With the [Plaid + Stripe ACH integration][2], exchange a Link `public_token`
and `account_id` for an API `access_token` and Stripe `bank_account_token`:
```go
client := plaid.NewClient(CLIENT_ID, SECRET, plaid.Tartan)
// POST /exchange_token
postRes, err := client.ExchangeTokenAccount(public_token, account_id)
if err != nil {
fmt.Println(err)
} else {
// Use the returned Plaid access_token to make Plaid API requests and the
// Stripe bank account token to make Stripe ACH API requests.
fmt.Println(postRes.AccessToken)
fmt.Println(postRes.BankAccountToken)
}
```
### Querying a category
```go
// GET /categories/13001001
category, err := plaid.GetCategory(plaid.Tartan, "13001001")
if err != nil {
fmt.Println(err)
} else {
fmt.Println("category", category.ID, "is", strings.Join(category.Hierarchy, ", "))
}
```
[1]: https://plaid.com/docs/link
[2]: https://plaid.com/docs/link/stripe

152
vendor/github.com/plaid/plaid-go/main.go generated vendored Normal file
View File

@ -0,0 +1,152 @@
package main
import (
"fmt"
"strings"
"github.com/plaid/plaid-go/plaid"
)
// main contains example usage of all functions
func main() {
// GET /institutions
res, err := plaid.GetInstitutions(plaid.Tartan)
if err != nil {
fmt.Println(err)
}
fmt.Println(res[0].Name, "has type:", res[0].Type)
// GET /institutions/5301a93ac140de84910000e0
inst, err := plaid.GetInstitution(plaid.Tartan, "5301a93ac140de84910000e0")
if err != nil {
fmt.Println("Institution Error:", err)
}
fmt.Println(inst.Name, "has mfa:", strings.Join(inst.MFA, ", "))
// GET /categories
categories, err := plaid.GetCategories(plaid.Tartan)
if err != nil {
fmt.Println(err)
}
fmt.Println("First category:", categories[0])
// GET /categories/13001001
category, err := plaid.GetCategory(plaid.Tartan, "13001001")
if err != nil {
fmt.Println(err)
}
fmt.Println("category", category.ID, "is", strings.Join(category.Hierarchy, ", "))
client := plaid.NewClient("test_id", "test_secret", plaid.Tartan)
// POST /auth
postRes, mfaRes, err :=
client.AuthAddUser("plaid_test", "plaid_good", "", "citi", nil)
if err != nil {
fmt.Println(err)
} else if mfaRes != nil {
switch mfaRes.Type {
case "device":
fmt.Println("--Device MFA--")
fmt.Println("Message:", mfaRes.Device.Message)
case "list":
fmt.Println("--List MFA--")
fmt.Println("Mask:", mfaRes.List[0].Mask, "\nType:", mfaRes.List[0].Type)
case "questions":
fmt.Println("--Questions MFA--")
fmt.Println("Question:", mfaRes.Questions[0].Question)
case "selections":
fmt.Println("--Selections MFA--")
fmt.Println("Question:", mfaRes.Selections[1].Question)
fmt.Println("Answers:", mfaRes.Selections[1].Answers)
}
postRes2, mfaRes2, err := client.AuthStepSendMethod(mfaRes.AccessToken, "type", "email")
if err != nil {
fmt.Println("Error submitting send_method", err)
}
fmt.Printf("%+v\n", mfaRes2)
fmt.Printf("%+v\n", postRes2)
postRes2, mfaRes2, err = client.AuthStep(mfaRes.AccessToken, "tomato")
if err != nil {
fmt.Println("Error submitting mfa", err)
}
fmt.Printf("%+v\n", mfaRes2)
fmt.Printf("%+v\n", postRes2)
} else {
fmt.Println(postRes.Accounts)
fmt.Println("Auth Get")
fmt.Println(client.AuthGet("test_citi"))
fmt.Println("Auth DELETE")
fmt.Println(client.AuthDelete("test_citi"))
}
// POST /connect
postRes, mfaRes, err = client.ConnectAddUser("plaid_test", "plaid_good", "", "citi", nil)
if err != nil {
fmt.Println(err)
} else if mfaRes != nil {
switch mfaRes.Type {
case "device":
fmt.Println("--Device MFA--")
fmt.Println("Message:", mfaRes.Device.Message)
case "list":
fmt.Println("--List MFA--")
fmt.Println("Mask:", mfaRes.List[0].Mask, "\nType:", mfaRes.List[0].Type)
case "questions":
fmt.Println("--Questions MFA--")
fmt.Println("Question:", mfaRes.Questions[0].Question)
case "selections":
fmt.Println("--Selections MFA--")
fmt.Println("Question:", mfaRes.Selections[1].Question)
fmt.Println("Answers:", mfaRes.Selections[1].Answers)
}
postRes2, mfaRes2, err := client.ConnectStepSendMethod(mfaRes.AccessToken, "type", "email")
if err != nil {
fmt.Println("Error submitting send_method", err)
}
fmt.Printf("%+v\n", mfaRes2)
fmt.Printf("%+v\n", postRes2)
postRes2, mfaRes2, err = client.ConnectStep(mfaRes.AccessToken, "1234")
if err != nil {
fmt.Println("Error submitting mfa", err)
}
fmt.Printf("%+v\n", mfaRes2)
fmt.Printf("%+v\n", postRes2)
} else {
fmt.Println(postRes.Accounts)
fmt.Println("Connect GET")
connectRes, _, _ := client.ConnectGet("test_citi", &plaid.ConnectGetOptions{true, "", "", ""})
fmt.Println(len(connectRes.Transactions))
fmt.Println(connectRes.Transactions)
fmt.Println("Connect DELETE")
fmt.Println(client.ConnectDelete("test_citi"))
}
// POST /balance
fmt.Println("Balance")
postRes, err = client.Balance("test_citi")
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", postRes)
// POST /upgrade
fmt.Println("Upgrade")
postRes, mfaRes, err = client.Upgrade("test_bofa", "connect", nil)
if err != nil {
fmt.Println(err)
}
fmt.Printf("%+v\n", mfaRes)
fmt.Printf("%+v\n", postRes)
// POST exchange_token
fmt.Println("ExchangeToken")
postRes, err = client.ExchangeToken("test,chase,connected")
fmt.Printf("%+v\n", postRes)
}

207
vendor/github.com/plaid/plaid-go/plaid/auth.go generated vendored Normal file
View File

@ -0,0 +1,207 @@
package plaid
import (
"bytes"
"encoding/json"
)
// AuthAddUser (POST /auth) submits a set of user credentials to add an Auth user.
//
// See https://plaid.com/docs/api/#add-auth-user.
func (c *Client) AuthAddUser(username, password, pin, institutionType string,
options *AuthOptions) (postRes *postResponse, mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(authJson{
ClientID: c.clientID,
Secret: c.secret,
Type: institutionType,
Username: username,
Password: password,
PIN: pin,
Options: options,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/auth", bytes.NewReader(jsonText))
}
// AuthStepSendMethod (POST /auth/step) specifies a particular send method for MFA,
// e.g. `{"mask":"xxx-xxx-5309"}`.
//
// See https://plaid.com/docs/api/#auth-mfa.
func (c *Client) AuthStepSendMethod(accessToken, key, value string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
sendMethod := map[string]string{key: value}
jsonText, err := json.Marshal(authStepSendMethodJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
Options: authStepOptions{sendMethod},
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/auth/step", bytes.NewReader(jsonText))
}
// AuthStep (POST /auth/step) submits an MFA answer for a given access token.
//
// See https://plaid.com/docs/api/#auth-mfa.
func (c *Client) AuthStep(accessToken, answer string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(authStepJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
MFA: answer,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/auth/step", bytes.NewReader(jsonText))
}
// AuthGet (POST /auth/get) retrieves account data for a given access token.
//
// See https://plaid.com/docs/api/#get-auth-data.
func (c *Client) AuthGet(accessToken string) (postRes *postResponse, err error) {
jsonText, err := json.Marshal(authGetJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
// /auth/get will never return an MFA response
postRes, _, err = c.postAndUnmarshal("/auth/get", bytes.NewReader(jsonText))
return postRes, err
}
// AuthUpdate (PATCH /auth) updates user credentials for a given access token.
//
// See https://plaid.com/docs/api/#update-auth-user.
func (c *Client) AuthUpdate(username, password, pin, accessToken string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(authUpdateJson{
ClientID: c.clientID,
Secret: c.secret,
Username: username,
Password: password,
PIN: pin,
AccessToken: accessToken,
})
if err != nil {
return nil, nil, err
}
return c.patchAndUnmarshal("/auth", bytes.NewReader(jsonText))
}
// AuthUpdateStep (PATCH /auth/step) updates user credentials and MFA for a given access token.
//
// See https://plaid.com/docs/api/#update-auth-user.
func (c *Client) AuthUpdateStep(username, password, pin, mfa, accessToken string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(authUpdateStepJson{
ClientID: c.clientID,
Secret: c.secret,
Username: username,
Password: password,
PIN: pin,
MFA: mfa,
AccessToken: accessToken,
})
if err != nil {
return nil, nil, err
}
return c.patchAndUnmarshal("/auth/step", bytes.NewReader(jsonText))
}
// AuthDelete (DELETE /auth) deletes data for a given access token.
//
// See https://plaid.com/docs/api/#delete-auth-user.
func (c *Client) AuthDelete(accessToken string) (deleteRes *deleteResponse, err error) {
jsonText, err := json.Marshal(authDeleteJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
return c.deleteAndUnmarshal("/auth", bytes.NewReader(jsonText))
}
// AuthOptions represents options associated with adding an Auth user.
//
// See https://plaid.com/docs/api/#add-auth-user.
type AuthOptions struct {
List bool `json:"list"`
}
type authJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Type string `json:"type"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
Options *AuthOptions `json:"options,omitempty"`
}
type authStepOptions struct {
SendMethod map[string]string `json:"send_method"`
}
type authStepSendMethodJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
Options authStepOptions `json:"options"`
}
type authStepJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
MFA string `json:"mfa"`
}
type authGetJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
}
type authUpdateJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
AccessToken string `json:"access_token"`
}
type authUpdateStepJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
MFA string `json:"mfa"`
AccessToken string `json:"access_token"`
}
type authDeleteJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
}

28
vendor/github.com/plaid/plaid-go/plaid/balance.go generated vendored Normal file
View File

@ -0,0 +1,28 @@
package plaid
import (
"bytes"
"encoding/json"
)
// Balance (POST /balance) retrieves real-time balance for a given access token.
//
// See https://plaid.com/docs/api/#balance.
func (c *Client) Balance(accessToken string) (postRes *postResponse, err error) {
jsonText, err := json.Marshal(balanceJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
postRes, _, err = c.postAndUnmarshal("/balance", bytes.NewReader(jsonText))
return postRes, err
}
type balanceJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
}

21
vendor/github.com/plaid/plaid-go/plaid/categories.go generated vendored Normal file
View File

@ -0,0 +1,21 @@
package plaid
// GetCategories returns information for all categories.
// See https://plaid.com/docs/api/#category-overview.
func GetCategories(environment environmentURL) (categories []category, err error) {
err = getAndUnmarshal(environment, "/categories", &categories)
return
}
// GetCategory returns information for a single category given an ID.
// See https://plaid.com/docs/api/#categories-by-id.
func GetCategory(environment environmentURL, id string) (cat category, err error) {
err = getAndUnmarshal(environment, "/categories/"+id, &cat)
return
}
type category struct {
Hierarchy []string `json:"hierarchy"` // e.g.: ["Food and Drink", "Bar"]
ID string `json:"id"` // e.g.: "13001000"
Type string `json:"type"` // e.g.: "place"
}

View File

@ -0,0 +1,52 @@
package plaid
import (
"fmt"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestCategories(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "categories tests")
}
var _ = Describe("categories", func() {
Describe("GetCategories", func() {
It("returns non-empty array", func() {
categories, err := GetCategories(Tartan)
Expect(err).To(BeNil(), "err should be nil")
Expect(categories).ToNot(BeEmpty())
})
})
Describe("GetCategory", func() {
It("returns proper fields", func() {
c, err := GetCategory(Tartan, "13001000")
Expect(err).To(BeNil(), "err should be nil")
Expect(c.Hierarchy).ToNot(BeEmpty())
Expect(c.Hierarchy[0]).To(Equal("Food and Drink"))
Expect(c.Hierarchy[1]).To(Equal("Bar"))
Expect(c.ID).To(Equal("13001000"))
Expect(c.Type).To(Equal("place"))
})
})
})
func ExampleGetCategory() {
category, err := GetCategory(Tartan, "13005006")
fmt.Println(err)
fmt.Println(category.Hierarchy[2])
fmt.Println(category.Type)
// Output: <nil>
// Sushi
// place
}

223
vendor/github.com/plaid/plaid-go/plaid/connect.go generated vendored Normal file
View File

@ -0,0 +1,223 @@
package plaid
import (
"bytes"
"encoding/json"
)
// ConnectAddUser (POST /connect) submits a set of user credentials to add a Connect user.
//
// See https://plaid.com/docs/api/#add-user.
func (c *Client) ConnectAddUser(username, password, pin, institutionType string,
options *ConnectOptions) (postRes *postResponse, mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(connectJson{
ClientID: c.clientID,
Secret: c.secret,
Type: institutionType,
Username: username,
Password: password,
PIN: pin,
Options: options,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/connect", bytes.NewReader(jsonText))
}
// ConnectStepSendMethod (POST /connect/step) specifies a particular send method for MFA,
// e.g. `{"mask":"xxx-xxx-5309"}`.
//
// See https://plaid.com/docs/api/#mfa-authentication.
func (c *Client) ConnectStepSendMethod(accessToken, key, value string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
sendMethod := map[string]string{key: value}
jsonText, err := json.Marshal(connectStepSendMethodJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
Options: connectStepOptions{sendMethod},
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/connect/step", bytes.NewReader(jsonText))
}
// ConnectStep (POST /connect/step) submits an MFA answer for a given access token.
//
// See https://plaid.com/docs/api/#mfa-authentication.
func (c *Client) ConnectStep(accessToken, answer string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(connectStepJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
MFA: answer,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/connect/step", bytes.NewReader(jsonText))
}
// ConnectGet (POST /connect/get) retrieves account and transaction data for a given access token.
//
// See https://plaid.com/docs/api/#get-transactions.
func (c *Client) ConnectGet(accessToken string, options *ConnectGetOptions) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(connectGetJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
Options: options,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/connect/get", bytes.NewReader(jsonText))
}
// ConnectUpdate (PATCH /connect) updates user credentials for a given access token.
//
// See https://plaid.com/docs/api/#update-user.
func (c *Client) ConnectUpdate(username, password, pin, accessToken string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(connectUpdateJson{
ClientID: c.clientID,
Secret: c.secret,
Username: username,
Password: password,
PIN: pin,
AccessToken: accessToken,
})
if err != nil {
return nil, nil, err
}
return c.patchAndUnmarshal("/connect", bytes.NewReader(jsonText))
}
// ConnectUpdateStep (PATCH /connect/step) updates user credentials and MFA for a given access token.
//
// See https://plaid.com/docs/api/#update-user.
func (c *Client) ConnectUpdateStep(username, password, pin, mfa, accessToken string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(connectUpdateStepJson{
ClientID: c.clientID,
Secret: c.secret,
Username: username,
Password: password,
PIN: pin,
MFA: mfa,
AccessToken: accessToken,
})
if err != nil {
return nil, nil, err
}
return c.patchAndUnmarshal("/connect/step", bytes.NewReader(jsonText))
}
// ConnectDelete (DELETE /connect) deletes data for a given access token.
//
// See https://plaid.com/docs/api/#delete-user.
func (c *Client) ConnectDelete(accessToken string) (deleteRes *deleteResponse, err error) {
jsonText, err := json.Marshal(connectDeleteJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
})
if err != nil {
return nil, err
}
return c.deleteAndUnmarshal("/connect", bytes.NewReader(jsonText))
}
// ConnectOptions represents options associated with adding an Connect user.
//
// See https://plaid.com/docs/api/#add-user.
type ConnectOptions struct {
Webhook string `json:"webhook,omitempty"`
Pending bool `json:"pending,omitempty"`
LoginOnly bool `json:"login_only,omitempty"`
List bool `json:"list,omitempty"`
StartDate string `json:"start_date,omitempty"`
EndDate string `json:"end_date,omitempty"`
}
type connectJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Type string `json:"type"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
Options *ConnectOptions `json:"options,omitempty"`
}
type connectStepOptions struct {
SendMethod map[string]string `json:"send_method"`
}
type connectStepSendMethodJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
Options connectStepOptions `json:"options"`
}
type connectStepJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
MFA string `json:"mfa"`
}
// ConnectGetOptions represents options associated with retrieving a Connect user.
//
// See https://plaid.com/docs/api/#retrieve-transactions.
type ConnectGetOptions struct {
Pending bool `json:"pending,omitempty"`
Account string `json:"account,omitempty"`
GTE string `json:"gte,omitempty"`
LTE string `json:"lte,omitempty"`
}
type connectGetJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
Options *ConnectGetOptions `json:"options,omitempty"`
}
type connectUpdateJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
AccessToken string `json:"access_token"`
}
type connectUpdateStepJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
Username string `json:"username"`
Password string `json:"password"`
PIN string `json:"pin,omitempty"`
MFA string `json:"mfa"`
AccessToken string `json:"access_token"`
}
type connectDeleteJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
}

20
vendor/github.com/plaid/plaid-go/plaid/errors.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
package plaid
import (
"fmt"
)
type plaidError struct {
// List of all errors: https://github.com/plaid/support/blob/master/errors.md
ErrorCode int `json:"code"`
Message string `json:"message"`
Resolve string `json:"resolve"`
// StatusCode needs to manually set from the http response
StatusCode int
}
func (e plaidError) Error() string {
return fmt.Sprintf("Plaid Error - http status: %d, code: %d, message: %s, resolve: %s",
e.StatusCode, e.ErrorCode, e.Message, e.Resolve)
}

View File

@ -0,0 +1,51 @@
package plaid
import (
"bytes"
"encoding/json"
)
// ExchangeToken (POST /exchange_token) exchanges a public token for an access token.
//
// See https://github.com/plaid/link
func (c *Client) ExchangeToken(publicToken string) (postRes *postResponse, err error) {
jsonText, err := json.Marshal(exchangeJson{
ClientID: c.clientID,
Secret: c.secret,
PublicToken: publicToken,
})
if err != nil {
return nil, err
}
postRes, _, err = c.postAndUnmarshal("/exchange_token", bytes.NewReader(jsonText))
return postRes, err
}
// ExchangeTokenAccount (POST /exchange_token) exchanges a public token and account id to receive a
// bank account token.
func (c *Client) ExchangeTokenAccount(publicToken string, accountId string) (postRes *postResponse, err error) {
jsonText, err := json.Marshal(exchangeAccountJson{
ClientID: c.clientID,
Secret: c.secret,
PublicToken: publicToken,
AccountId: accountId,
})
if err != nil {
return nil, err
}
postRes, _, err = c.postAndUnmarshal("/exchange_token", bytes.NewReader(jsonText))
return postRes, err
}
type exchangeJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
PublicToken string `json:"public_token"`
}
type exchangeAccountJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
PublicToken string `json:"public_token"`
AccountId string `json:"account_id"`
}

View File

@ -0,0 +1,24 @@
package plaid
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestExchangeToken(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "exchange-token tests")
}
var _ = Describe("exchange-token", func() {
Describe("ExchangeToken", func() {
It("returns public_token and access_token", func() {
c := NewClient("test_id", "test_secret", Tartan)
res, err := c.ExchangeToken("test,chase,connected")
Expect(err).To(BeNil(), "err should be nil")
Expect(res.AccessToken).To(Equal("test_chase"))
})
})
})

29
vendor/github.com/plaid/plaid-go/plaid/institutions.go generated vendored Normal file
View File

@ -0,0 +1,29 @@
package plaid
// GetInstitution returns information for a single institution given an ID.
// See https://plaid.com/docs/api/#institutions-by-id.
func GetInstitution(environment environmentURL, id string) (inst institution, err error) {
err = getAndUnmarshal(environment, "/institutions/"+id, &inst)
return
}
// GetInstitution returns information for all institutions.
// See https://plaid.com/docs/api/#all-institutions.
func GetInstitutions(environment environmentURL) (institutions []institution, err error) {
err = getAndUnmarshal(environment, "/institutions", &institutions)
return
}
type institution struct {
Credentials struct {
Password string `json:"password"` // e.g.: "Password"
PIN string `json:"pin"` // e.g.: "PIN"
Username string `json:"username"` // e.g.: "Online ID"
}
Name string `json:"name"` // e.g.: "Bank of America"
HasMFA bool `json:"has_mfa"` // e.g.: true
ID string `json:"id"` // e.g.: "5301a93ac140de84910000e0"
MFA []string `json:"mfa"` // e.g.: ["code", "list", "questions"]
Products []string `json:"products"` // e.g.: ["connect", "auth", "balance"]
Type string `json:"type"` // e.g.: "bofa"
}

View File

@ -0,0 +1,55 @@
package plaid
import (
"fmt"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestInstitutions(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "institutions tests")
}
var _ = Describe("institutions", func() {
Describe("GetInstitutions", func() {
It("returns non-empty array", func() {
institutions, err := GetInstitutions(Tartan)
Expect(err).To(BeNil(), "err should be nil")
Expect(institutions).ToNot(BeEmpty())
})
})
Describe("GetInstitution", func() {
It("returns proper fields", func() {
i, err := GetInstitution(Tartan, "5301a9d704977c52b60000db")
Expect(err).To(BeNil(), "err should be nil")
Expect(i.HasMFA).To(BeFalse())
Expect(i.ID).To(Equal("5301a9d704977c52b60000db"))
Expect(i.MFA).To(BeEmpty())
Expect(i.Name).To(Equal("American Express"))
Expect(i.Type).To(Equal("amex"))
Expect(i.Products).ToNot(BeEmpty())
Expect(i.Products).To(ContainElement("balance"))
Expect(i.Products).To(ContainElement("connect"))
})
})
})
func ExampleGetInstitution() {
institution, err := GetInstitution(Tartan, "5301a9d704977c52b60000db")
fmt.Println(err)
fmt.Println(institution.Name)
fmt.Println(institution.Type)
// Output: <nil>
// American Express
// amex
}

373
vendor/github.com/plaid/plaid-go/plaid/plaid.go generated vendored Normal file
View File

@ -0,0 +1,373 @@
// Package plaid implements a Go client for the Plaid API (https://plaid.com/docs)
package plaid
import (
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
)
// NewClient instantiates a Client associated with a client id, secret and environment.
// See https://plaid.com/docs/api/#gaining-access.
func NewClient(clientID, secret string, environment environmentURL) *Client {
return &Client{clientID, secret, environment, &http.Client{}}
}
// Same as above but with additional parameter to pass http.Client. This is required
// if you want to run the code on Google AppEngine which prohibits use of http.DefaultClient
func NewCustomClient(clientID, secret string, environment environmentURL, httpClient *http.Client) *Client {
return &Client{clientID, secret, environment, httpClient}
}
// Note: Client is only exported for method documentation purposes.
// Instances should only be created through the 'NewClient' function.
//
// See https://github.com/golang/go/issues/7823.
type Client struct {
clientID string
secret string
environment environmentURL
httpClient *http.Client
}
type environmentURL string
var Tartan environmentURL = "https://tartan.plaid.com"
var Production environmentURL = "https://api.plaid.com"
type Account struct {
ID string `json:"_id"`
ItemID string `json:"_item"`
UserID string `json:"_user"`
Balance struct {
Available float64 `json:"available"`
Current float64 `json:"current"`
} `json:"balance"`
Meta struct {
Number string `json:"number"`
Name string `json:"name"`
} `json:"meta"`
Numbers struct {
Account string `json:"account"`
Routing string `json:"routing"`
WireRouting string `json:"wireRouting"`
} `json:"numbers"`
Type string `json:"type"`
InstitutionType string `json:"institution_type"`
}
type Transaction struct {
ID string `json:"_id"`
AccountID string `json:"_account"`
Amount float64 `json:"amount"`
Date string `json:"date"`
Name string `json:"name"`
Meta struct {
AccountOwner string `json:"account_owner"`
Location struct {
Address string `json:"address"`
City string `json:"city"`
Coordinates struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
} `json:"coordinates"`
State string `json:"state"`
Zip string `json:"zip"`
} `json:"location"`
} `json:"meta"`
Pending bool `json:"pending"`
Type struct {
Primary string `json:"primary"`
} `json:"type"`
Category []string `json:"category"`
CategoryID string `json:"category_id"`
Score struct {
Location struct {
Address float64 `json:"address"`
City float64 `json:"city"`
State float64 `json:"state"`
Zip float64 `json:"zip"`
}
Name float64 `json:"name"`
} `json:"score"`
}
type mfaIntermediate struct {
AccessToken string `json:"access_token"`
MFA interface{} `json:"mfa"`
Type string `json:"type"`
}
type mfaDevice struct {
Message string
}
type mfaList struct {
Mask string
Type string
}
type mfaQuestion struct {
Question string
}
type mfaSelection struct {
Answers []string
Question string
}
// 'mfa' contains the union of all possible mfa types
// Users should switch on the 'Type' field
type mfaResponse struct {
AccessToken string
Type string
Device mfaDevice
List []mfaList
Questions []mfaQuestion
Selections []mfaSelection
}
type postResponse struct {
// Normal response fields
AccessToken string `json:"access_token"`
AccountId string `json:"account_id"`
Accounts []Account `json:"accounts"`
BankAccountToken string `json:"stripe_bank_account_token"`
MFA string `json:"mfa"`
Transactions []Transaction `json:"transactions"`
}
type deleteResponse struct {
Message string `json:"message"`
}
// getAndUnmarshal is not a method because no client authentication is required
func getAndUnmarshal(environment environmentURL, endpoint string, structure interface{}) error {
res, err := http.Get(string(environment) + endpoint)
if err != nil {
return err
}
raw, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
res.Body.Close()
// Successful response
if res.StatusCode == 200 {
if err = json.Unmarshal(raw, structure); err != nil {
return err
}
return nil
}
// Attempt to unmarshal into Plaid error format
var plaidErr plaidError
if err = json.Unmarshal(raw, &plaidErr); err != nil {
return err
}
plaidErr.StatusCode = res.StatusCode
return plaidErr
}
func (c *Client) postAndUnmarshal(endpoint string,
body io.Reader) (*postResponse, *mfaResponse, error) {
// Read response body
req, err := http.NewRequest("POST", string(c.environment)+endpoint, body)
if err != nil {
return nil, nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", "plaid-go")
res, err := c.httpClient.Do(req)
if err != nil {
return nil, nil, err
}
raw, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, nil, err
}
res.Body.Close()
return unmarshalPostMFA(res, raw)
}
func (c *Client) patchAndUnmarshal(endpoint string,
body io.Reader) (*postResponse, *mfaResponse, error) {
req, err := http.NewRequest("PATCH", string(c.environment)+endpoint, body)
if err != nil {
return nil, nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", "plaid-go")
res, err := c.httpClient.Do(req)
if err != nil {
return nil, nil, err
}
raw, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, nil, err
}
res.Body.Close()
return unmarshalPostMFA(res, raw)
}
func (c *Client) deleteAndUnmarshal(endpoint string,
body io.Reader) (*deleteResponse, error) {
req, err := http.NewRequest("DELETE", string(c.environment)+endpoint, body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", "plaid-go")
res, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
raw, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
res.Body.Close()
// Successful response
var deleteRes deleteResponse
if res.StatusCode == 200 {
if err = json.Unmarshal(raw, &deleteRes); err != nil {
return nil, err
}
return &deleteRes, nil
}
// Attempt to unmarshal into Plaid error format
var plaidErr plaidError
if err = json.Unmarshal(raw, &plaidErr); err != nil {
return nil, err
}
plaidErr.StatusCode = res.StatusCode
return nil, plaidErr
}
// Unmarshals response into postResponse, mfaResponse, or plaidError
func unmarshalPostMFA(res *http.Response, body []byte) (*postResponse, *mfaResponse, error) {
// Different marshaling cases
var mfaInter mfaIntermediate
var postRes postResponse
var err error
switch {
// Successful response
case res.StatusCode == 200:
if err = json.Unmarshal(body, &postRes); err != nil {
return nil, nil, err
}
return &postRes, nil, nil
// MFA case
case res.StatusCode == 201:
if err = json.Unmarshal(body, &mfaInter); err != nil {
return nil, nil, err
}
mfaRes := mfaResponse{Type: mfaInter.Type, AccessToken: mfaInter.AccessToken}
switch mfaInter.Type {
case "device":
temp, ok := mfaInter.MFA.(interface{})
if !ok {
return nil, nil, errors.New("Could not decode device mfa")
}
deviceStruct, ok := temp.(map[string]interface{})
if !ok {
return nil, nil, errors.New("Could not decode device mfa")
}
deviceText, ok := deviceStruct["message"].(string)
if !ok {
return nil, nil, errors.New("Could not decode device mfa")
}
mfaRes.Device.Message = deviceText
case "list":
temp, ok := mfaInter.MFA.([]interface{})
if !ok {
return nil, nil, errors.New("Could not decode list mfa")
}
for _, v := range temp {
listArray, ok := v.(map[string]interface{})
if !ok {
return nil, nil, errors.New("Could not decode list mfa")
}
maskText, ok := listArray["mask"].(string)
if !ok {
return nil, nil, errors.New("Could not decode list mfa")
}
typeText, ok := listArray["type"].(string)
if !ok {
return nil, nil, errors.New("Could not decode list mfa")
}
mfaRes.List = append(mfaRes.List, mfaList{Mask: maskText, Type: typeText})
}
case "questions":
questions, ok := mfaInter.MFA.([]interface{})
if !ok {
return nil, nil, errors.New("Could not decode questions mfa")
}
for _, v := range questions {
q, ok := v.(map[string]interface{})
if !ok {
return nil, nil, errors.New("Could not decode questions mfa")
}
questionText, ok := q["question"].(string)
if !ok {
return nil, nil, errors.New("Could not decode questions mfa question")
}
mfaRes.Questions = append(mfaRes.Questions, mfaQuestion{Question: questionText})
}
case "selections":
selections, ok := mfaInter.MFA.([]interface{})
if !ok {
return nil, nil, errors.New("Could not decode selections mfa")
}
for _, v := range selections {
s, ok := v.(map[string]interface{})
if !ok {
return nil, nil, errors.New("Could not decode selections mfa")
}
tempAnswers, ok := s["answers"].([]interface{})
if !ok {
return nil, nil, errors.New("Could not decode selections answers")
}
answers := make([]string, len(tempAnswers))
for i, a := range tempAnswers {
answers[i], ok = a.(string)
}
if !ok {
return nil, nil, errors.New("Could not decode selections answers")
}
question, ok := s["question"].(string)
if !ok {
return nil, nil, errors.New("Could not decode selections questions")
}
mfaRes.Selections = append(mfaRes.Selections, mfaSelection{Answers: answers, Question: question})
}
}
return nil, &mfaRes, nil
// Error case, attempt to unmarshal into Plaid error format
case res.StatusCode >= 400:
var plaidErr plaidError
if err = json.Unmarshal(body, &plaidErr); err != nil {
return nil, nil, err
}
plaidErr.StatusCode = res.StatusCode
return nil, nil, plaidErr
}
return nil, nil, errors.New("Unknown Plaid Error - Status:" + string(res.StatusCode))
}

98
vendor/github.com/plaid/plaid-go/plaid/upgrade.go generated vendored Normal file
View File

@ -0,0 +1,98 @@
package plaid
import (
"bytes"
"encoding/json"
)
// Upgrade (POST /upgrade) upgrades an access token to an additional product.
//
// See https://plaid.com/docs/api/#upgrade-user.
func (c *Client) Upgrade(accessToken, upgradeTo string,
options *UpgradeOptions) (postRes *postResponse, mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(upgradeJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
UpgradeTo: upgradeTo,
Options: options,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/upgrade", bytes.NewReader(jsonText))
}
// UpgradeStepSendMethod (POST /upgrade/step) specifies a particular send method for MFA,
// e.g. {"mask":"xxx-xxx-5309"}.
//
// See https://plaid.com/docs/api/#upgrade-user.
func (c *Client) UpgradeStepSendMethod(accessToken, key, value string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
sendMethod := map[string]string{key: value}
jsonText, err := json.Marshal(upgradeStepSendMethodJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
Options: upgradeStepOptions{sendMethod},
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/upgrade/step", bytes.NewReader(jsonText))
}
// UpgradeStep (POST /upgrade/step) submits an MFA answer for a given access token.
//
// See https://plaid.com/docs/api/#mfa-authentication for upgrades to Connect.
// See https://plaid.com/docs/api/#mfa-auth for upgrades to Auth.
func (c *Client) UpgradeStep(accessToken, answer string) (postRes *postResponse,
mfaRes *mfaResponse, err error) {
jsonText, err := json.Marshal(upgradeStepJson{
ClientID: c.clientID,
Secret: c.secret,
AccessToken: accessToken,
MFA: answer,
})
if err != nil {
return nil, nil, err
}
return c.postAndUnmarshal("/upgrade/step", bytes.NewReader(jsonText))
}
// UpgradeOptions represents options associated with upgrading a user.
//
// See https://plaid.com/docs/api/#add-user for upgrades to Connect.
// See https://plaid.com/docs/api/#add-auth-user for upgrades to Auth.
type UpgradeOptions struct {
Webhook string `json:"webhook,omitempty"`
}
type upgradeJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
UpgradeTo string `json:"upgrade_to"`
Options *UpgradeOptions `json:"options,omitempty"`
}
type upgradeStepOptions struct {
SendMethod map[string]string `json:"send_method"`
}
type upgradeStepSendMethodJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
Options upgradeStepOptions `json:"options"`
}
type upgradeStepJson struct {
ClientID string `json:"client_id"`
Secret string `json:"secret"`
AccessToken string `json:"access_token"`
MFA string `json:"mfa"`
}