-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathintuit.go
307 lines (251 loc) · 8.51 KB
/
intuit.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/*
Go client for Intuit's Customer Account Data API
*/
package intuit
import (
"encoding/xml"
"fmt"
"github.com/MattNewberry/oauth"
"time"
)
const (
InstitutionXMLNS = "http://schema.intuit.com/platform/fdatafeed/institutionlogin/v1"
ChallengeXMLNS = "http://schema.intuit.com/platform/fdatafeed/challenge/v1"
BaseURL = "https://financialdatafeed.platform.intuit.com/v1/"
GET = "GET"
POST = "POST"
DELETE = "DELETE"
PUT = "PUT"
updateLoginType = 1 + iota
discoverAndAddType
)
var SessionConfiguration *Configuration
type challengeContextType int
type InstitutionLogin struct {
XMLName xml.Name `xml:"InstitutionLogin"`
XMLNS string `xml:"xmlns,attr"`
Credentials Credentials `xml:"credentials,omitempty"`
}
type InstitutionLoginMFA struct {
XMLName xml.Name `xml:"InstitutionLogin"`
XMLNS string `xml:"xmlns,attr"`
ChallengeResponses ChallengeResponses `xml:"challengeResponses"`
}
type Credentials struct {
Credentials []Credential
}
type Credential struct {
XMLName xml.Name `xml:"credential"`
Name string `xml:"name"`
Value string `xml:"value"`
}
type ChallengeResponses struct {
ChallengeResponses []ChallengeResponse
}
type Challenge struct {
Question string
Choices []Choice
}
type ChallengeResponse struct {
XMLName xml.Name `xml:"v11:response"`
XMLNS string `xml:"xmlns:v11,attr"`
Answer interface{} `xml:",innerxml"`
}
type Choice struct {
Value interface{}
Text string
}
type ChallengeSession struct {
InstitutionId string
LoginId string
SessionId string
NodeId string
Challenges []Challenge
Answers []interface{}
contextType challengeContextType
}
type Configuration struct {
CustomerId string
OAuthConsumerKey string
OAuthConsumerSecret string
oAuthToken *oauth.AccessToken
SamlProviderId string
CertificatePath string
}
/*
Configure the client for access to your application.
*/
func Configure(configuration *Configuration) {
SessionConfiguration = configuration
}
/*
Set the customer Id for the current session.
*/
func Scope(id string) {
if SessionConfiguration == nil {
SessionConfiguration = &Configuration{}
}
SessionConfiguration.CustomerId = id
}
/*
Discover new accounts for a customer, returning an MFA response if applicable.
In practice, the most efficient workflow is to cache the Institutions list and pass the username and password keys to this method. Without doing so, fetching the instituion's details will be required.
*/
func DiscoverAndAddAccounts(institutionId string, username string, password string, usernameKey string, passwordKey string) (accounts []interface{}, challengeSession *ChallengeSession, err error) {
userCredential := Credential{Name: usernameKey, Value: username}
passwordCredential := Credential{Name: passwordKey, Value: password}
credentials := Credentials{Credentials: []Credential{userCredential, passwordCredential}}
payload := &InstitutionLogin{Credentials: credentials, XMLNS: InstitutionXMLNS}
data, err := post(fmt.Sprintf("institutions/%v/logins", institutionId), payload, nil, nil)
if err == nil {
// Success
accounts = data.(map[string]interface{})["accounts"].([]interface{})
} else if data != nil {
challengeSession = parseChallengeSession(discoverAndAddType, data, err)
challengeSession.InstitutionId = institutionId
}
return
}
/*
Update login information for an account, returning an MFA response if applicable.
*/
func UpdateLoginAccount(loginId string, username string, password string, usernameKey string, passwordKey string) (accounts []interface{}, challengeSession *ChallengeSession, err error) {
userCredential := Credential{Name: usernameKey, Value: username}
passwordCredential := Credential{Name: passwordKey, Value: password}
credentials := Credentials{Credentials: []Credential{userCredential, passwordCredential}}
payload := &InstitutionLogin{Credentials: credentials, XMLNS: InstitutionXMLNS}
data, err := put(fmt.Sprintf("logins/%v?refresh=true", loginId), payload, nil, nil)
if err == nil {
// Success
accounts = data.(map[string]interface{})["accounts"].([]interface{})
} else if data != nil {
challengeSession = parseChallengeSession(updateLoginType, data, err)
challengeSession.LoginId = loginId
}
return
}
/*
Return all accounts stored for the scoped customer.
*/
func LoginAccounts(loginId string) ([]interface{}, error) {
res, err := get(fmt.Sprintf("logins/%v/accounts", loginId), nil)
data := res.(map[string]interface{})
return data["accounts"].([]interface{}), err
}
/*
When prompted with an MFA challenge, reply with an answer to the challenges.
*/
func RespondToChallenge(session *ChallengeSession) (data interface{}, err error) {
responses := make([]ChallengeResponse, len(session.Challenges))
for i, r := range session.Answers {
responses[i] = ChallengeResponse{Answer: r, XMLNS: ChallengeXMLNS}
}
response := ChallengeResponses{ChallengeResponses: responses}
payload := &InstitutionLoginMFA{ChallengeResponses: response, XMLNS: InstitutionXMLNS}
headers := map[string][]string{
"challengeNodeId": []string{session.NodeId},
"challengeSessionId": []string{session.SessionId},
}
switch session.contextType {
case discoverAndAddType:
data, err = post(fmt.Sprintf("institutions/%v/logins", session.InstitutionId), payload, nil, headers)
case updateLoginType:
data, err = put(fmt.Sprintf("logins/%v", session.LoginId), payload, nil, headers)
}
return
}
/*
Return all accounts stored for the scoped customer.
*/
func Accounts() ([]interface{}, error) {
res, err := get("accounts", nil)
data := res.(map[string]interface{})
return data["accounts"].([]interface{}), err
}
/*
Return a specific account for the scoped customer, given it's Id.
*/
func Account(accountId string) (map[string]interface{}, error) {
res, err := get(fmt.Sprintf("accounts/%s", accountId), nil)
data := res.(map[string]interface{})
account := data["accounts"].([]interface{})
return account[0].(map[string]interface{}), err
}
/*
Get all transactions for an account, filtered by the given start and end times.
*/
func Transactions(accountId string, start time.Time, end time.Time) (map[string]interface{}, error) {
params := make(map[string]string)
const timeFormat = "2006-01-02"
params["txnStartDate"] = start.Format(timeFormat)
params["tnxEndDate"] = end.Format(timeFormat)
res, err := get(fmt.Sprintf("accounts/%s/transactions", accountId), params)
var data map[string]interface{}
if err == nil {
data = res.(map[string]interface{})
}
return data, err
}
/*
Retrieve all known institutions.
Given the volume of institutions supported, this call can be very time consuming.
*/
func Institutions() ([]interface{}, error) {
res, err := get("institutions", nil)
data := res.(map[string]interface{})
all := data["institution"].([]interface{})
return all, err
}
/*
Retrieve an institution's detailed information.
*/
func Institution(institutionId string) (data map[string]interface{}, err error) {
res, err := get(fmt.Sprintf("institutions/%s", institutionId), nil)
if res != nil {
data = res.(map[string]interface{})
}
return
}
/*
Delete the scoped customer and all related accounts.
*/
func DeleteCustomer() error {
_, err := request(DELETE, "customers", "", nil, nil)
return err
}
/*
Delete an account for the scoped customer.
*/
func DeleteAccount(accountId string) error {
_, err := request(DELETE, "accounts/"+accountId, "", nil, nil)
return err
}
func parseChallengeSession(contextType challengeContextType, data interface{}, err error) *ChallengeSession {
challengeData := data.(map[string]interface{})
httpError := err.(oauth.HTTPExecuteError)
headers := httpError.ResponseHeaders
var challengeSession = &ChallengeSession{contextType: contextType}
challengeSession.SessionId = headers.Get("Challengesessionid")
challengeSession.NodeId = headers.Get("Challengenodeid")
challengeSession.Challenges = make([]Challenge, 0)
challenges := challengeData["challenge"].([]interface{})
for _, c := range challenges {
chal := c.(map[string]interface{})
for _, v := range chal {
vData := v.([]interface{})
challenge := Challenge{}
for i, val := range vData {
if i == 0 {
challenge.Question = val.(string)
challenge.Choices = make([]Choice, 0)
} else {
cData := val.(map[string]interface{})
choice := Choice{Value: cData["val"].(string), Text: cData["text"].(string)}
challenge.Choices = append(challenge.Choices, choice)
}
}
challengeSession.Challenges = append(challengeSession.Challenges, challenge)
}
}
return challengeSession
}