mirror of https://github.com/perkeep/perkeep.git
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:
commit
e0107f74bc
|
@ -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}", ""]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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"},
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
.DS_Store
|
|
@ -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.
|
|
@ -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
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"))
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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"`
|
||||
}
|
Loading…
Reference in New Issue