plaid 1.7.1 → 2.0.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +3 -0
  3. data/CONTRIBUTING.md +15 -0
  4. data/LICENSE +20 -0
  5. data/README.md +215 -63
  6. data/Rakefile +50 -4
  7. data/UPGRADING.md +45 -0
  8. data/bin/console +13 -0
  9. data/bin/setup +8 -0
  10. data/lib/plaid.rb +51 -88
  11. data/lib/plaid/account.rb +144 -0
  12. data/lib/plaid/category.rb +62 -0
  13. data/lib/plaid/client.rb +67 -0
  14. data/lib/plaid/connector.rb +168 -0
  15. data/lib/plaid/errors.rb +24 -14
  16. data/lib/plaid/income.rb +106 -0
  17. data/lib/plaid/info.rb +65 -0
  18. data/lib/plaid/institution.rb +240 -0
  19. data/lib/plaid/risk.rb +34 -0
  20. data/lib/plaid/transaction.rb +123 -0
  21. data/lib/plaid/user.rb +430 -0
  22. data/lib/plaid/version.rb +1 -1
  23. data/plaid.gemspec +20 -12
  24. metadata +58 -62
  25. data/.gitignore +0 -15
  26. data/.rspec +0 -2
  27. data/.travis.yml +0 -5
  28. data/LICENSE.txt +0 -22
  29. data/PUBLISHING.md +0 -21
  30. data/lib/plaid/config.rb +0 -19
  31. data/lib/plaid/connection.rb +0 -109
  32. data/lib/plaid/models/account.rb +0 -24
  33. data/lib/plaid/models/category.rb +0 -17
  34. data/lib/plaid/models/exchange_token_response.rb +0 -11
  35. data/lib/plaid/models/info.rb +0 -12
  36. data/lib/plaid/models/institution.rb +0 -22
  37. data/lib/plaid/models/transaction.rb +0 -24
  38. data/lib/plaid/models/user.rb +0 -189
  39. data/spec/plaid/config_spec.rb +0 -67
  40. data/spec/plaid/connection_spec.rb +0 -191
  41. data/spec/plaid/error_spec.rb +0 -10
  42. data/spec/plaid/models/account_spec.rb +0 -37
  43. data/spec/plaid/models/category_spec.rb +0 -16
  44. data/spec/plaid/models/institution_spec.rb +0 -19
  45. data/spec/plaid/models/transaction_spec.rb +0 -28
  46. data/spec/plaid/models/user_spec.rb +0 -172
  47. data/spec/plaid_spec.rb +0 -263
  48. data/spec/spec_helper.rb +0 -14
data/lib/plaid/risk.rb ADDED
@@ -0,0 +1,34 @@
1
+ module Plaid
2
+ # Public: Representation of risk data (per account).
3
+ class Risk
4
+ # Public: The Float comprehensive risk score associated with the account,
5
+ # where 0 is the lowest risk and 1 is the highest. E.g. 0.5.
6
+ attr_reader :score
7
+
8
+ # Public: The Hash with Symbol reasons and associated Float subscores
9
+ # contributing to the overall risk score.
10
+ #
11
+ # E.g. { transaction_amounts: 0.78, foreign_fees: 0.96, ... }.
12
+ attr_reader :reason
13
+
14
+ # Internal: Construct a Risk object.
15
+ #
16
+ # fields - The Hash with fields.
17
+ def initialize(fields)
18
+ @score = fields['score']
19
+ @reason = Plaid.symbolize_hash(fields['reason'])
20
+ end
21
+
22
+ # Public: Get a String representation of Risk.
23
+ #
24
+ # Returns a String.
25
+ def inspect
26
+ "#<Plaid::Risk score=#{score.inspect}, ..."
27
+ end
28
+
29
+ # Public: Get a String representation of Risk.
30
+ #
31
+ # Returns a String.
32
+ alias to_s inspect
33
+ end
34
+ end
@@ -0,0 +1,123 @@
1
+ # coding: utf-8
2
+ require 'date'
3
+
4
+ module Plaid
5
+ # Public: Representation of a transaction.
6
+ class Transaction
7
+ # Public: The String unique ID of the transaction. E.g.
8
+ # "0AZ0De04KqsreDgVwM1RSRYjyd8yXxSDQ8Zxn".
9
+ attr_reader :id
10
+
11
+ # Public: The String ID of the account in which this transaction occurred.
12
+ # E.g. "XARE85EJqKsjxLp6XR8ocg8VakrkXpTXmRdOo".
13
+ attr_reader :account_id
14
+
15
+ # Public: The Date that the transaction took place.
16
+ # E.g. #<Date: 2016-04-28 ...>
17
+ attr_reader :date
18
+
19
+ # Public: The settled dollar value as Float. Positive values when
20
+ # money moves out of the account; negative values when money moves
21
+ # in. E.g. 0.10 (10 cents).
22
+ attr_reader :amount
23
+
24
+ # Public: The String descriptive name of the transaction. E.g.
25
+ # "Golden Crepes".
26
+ attr_reader :name
27
+
28
+ # Public: The Hash with additional information regarding the
29
+ # transaction. The keys are Symbols. E.g.
30
+ # { location: { "city": "San Francisco", "state": "CA" } }
31
+ attr_reader :meta
32
+
33
+ # Public: The Hash with location information obtained from the
34
+ # meta field. The keys are Strings.
35
+ #
36
+ # E.g. {"address" => "262 W 15th St",
37
+ # "city" => "New York",
38
+ # "state" => "NY",
39
+ # "zip" => "10011",
40
+ # "coordinates" => {
41
+ # "lat" => 40.740352,
42
+ # "lon" => -74.001761
43
+ # }}
44
+ attr_reader :location
45
+
46
+ # Public: The Boolean flag which identifies the transaction as
47
+ # pending or unsettled.
48
+ attr_reader :pending
49
+
50
+ # Public: The Hash with numeric representation of Plaid confidence
51
+ # in the meta data attached to the transaction. In the case of a
52
+ # score <.9 Plaid will default to guaranteed and known
53
+ # information. E.g.
54
+ #
55
+ # {location: {
56
+ # "address" => 1,
57
+ # "city" => 1,
58
+ # "state" => 1
59
+ # }, name: 0.9}.
60
+ attr_reader :score
61
+
62
+ # Public: The Hash transaction type.
63
+ #
64
+ # E.g. {:primary => :place}.
65
+ attr_reader :type
66
+
67
+ # Public: The Array of String category hierarchy.
68
+ #
69
+ # E.g. ["Food and Drink", "Restaurants"].
70
+ attr_reader :category_hierarchy
71
+
72
+ # Public: The String Category ID.
73
+ #
74
+ # E.g. "13005000".
75
+ attr_reader :category_id
76
+
77
+ # Public: The String ID of a posted transaction's associated
78
+ # pending transaction - where applicable.
79
+ attr_reader :pending_transaction_id
80
+
81
+ # Public: Initialize a Transaction instance.
82
+ #
83
+ # fields - The Hash with fields.
84
+ def initialize(fields)
85
+ @id = fields['_id']
86
+ @account_id = fields['_account']
87
+
88
+ @date = fields['date'] && Date.parse(fields['date'])
89
+ @amount = fields['amount']
90
+ @name = fields['name']
91
+ @meta = Plaid.symbolize_hash(fields['meta'])
92
+ @location = (@meta && @meta[:location]) || {}
93
+ @pending = fields['pending']
94
+ @pending_transaction_id = fields['_pendingTransaction']
95
+ @score = Plaid.symbolize_hash(fields['score'])
96
+
97
+ @type = Plaid.symbolize_hash(fields['type'], values: true)
98
+ @category_hierarchy = fields['category']
99
+ @category_id = fields['category_id']
100
+ end
101
+
102
+ # Public: Detect if the transaction is pending or unsettled.
103
+ #
104
+ # Returns true if it is.
105
+ def pending?
106
+ pending
107
+ end
108
+
109
+ # Public: Get a String representation of the transaction.
110
+ #
111
+ # Returns a String.
112
+ def inspect
113
+ "#<Plaid::Transaction id=#{id.inspect}, account_id=#{id.inspect}, " \
114
+ "date=#{date}, amount=#{amount.inspect}, name=#{name.inspect}, " \
115
+ "pending=#{pending.inspect}>"
116
+ end
117
+
118
+ # Public: Get a String representation of the transaction.
119
+ #
120
+ # Returns a String.
121
+ alias to_s inspect
122
+ end
123
+ end
data/lib/plaid/user.rb ADDED
@@ -0,0 +1,430 @@
1
+ require_relative 'account'
2
+
3
+ module Plaid
4
+ # Public: A class which encapsulates the authenticated user for all Plaid
5
+ # products.
6
+ class User
7
+ # Public: The access token for authenticated user.
8
+ attr_reader :access_token
9
+
10
+ # Public: The current product. Provides a context for #update and #delete
11
+ # calls. See Plaid::PRODUCTS.
12
+ attr_reader :product
13
+
14
+ # Public: The Array of Account instances providing accounts information
15
+ # for the user.
16
+ attr_reader :accounts
17
+
18
+ # Public: The Array of Transactions provided by initial call to User.create.
19
+ #
20
+ # If the :login_only option of User.create is set to false, the initial
21
+ # 30-day transactional data are returned during the API call. This attribute
22
+ # contains them.
23
+ attr_reader :initial_transactions
24
+
25
+ # Public: The Symbol MFA type to be used (or nil, if no MFA required).
26
+ #
27
+ # E.g. :questions, :list, or :device.
28
+ attr_reader :mfa_type
29
+
30
+ # Public: The MFA data (Hash or Array of Hash) or nil, if no MFA required.
31
+ #
32
+ # E.g. [{ question: "What was the name of your first pet?" }]
33
+ # or
34
+ # [{ mask: 't..t@plaid.com', type: 'email' },
35
+ # { mask: 'xxx-xxx-5309', type: 'phone' }]
36
+ # or
37
+ # { message: 'Code sent to xxx-xxx-5309' }
38
+ attr_reader :mfa
39
+
40
+ # Internal: The Plaid::Client instance used to make queries.
41
+ attr_reader :client
42
+
43
+ # Public: Create (add) a user.
44
+ #
45
+ # product - The Symbol product name you are adding the user to, one of
46
+ # Plaid::PRODUCTS (e.g. :info, :connect, etc.).
47
+ # institution - The String/Symbol financial institution type that you
48
+ # want to access (e.g. :wells).
49
+ # username - The String username associated with the financial
50
+ # institution.
51
+ # password - The String password associated with the financial
52
+ # institution.
53
+ # pin - The String PIN number associated with the financial
54
+ # institution (default: nil).
55
+ # options - the Hash options (default: {}):
56
+ # :list - The Boolean flag which would request the
57
+ # available send methods if the institution
58
+ # requires code-based MFA credential (default:
59
+ # false).
60
+ # :webhook - The String webhook URL. Used with :connect,
61
+ # :income, and :risk products (default: nil).
62
+ # :pending - The Boolean flag requesting to return
63
+ # pending transactions. Used with :connect
64
+ # product (default: false).
65
+ # :login_only - The Boolean option valid for initial
66
+ # authentication only. If set to false, the
67
+ # initial request will return transaction data
68
+ # based on the start_date and end_date.
69
+ # :start_date - The start Date from which to return
70
+ # transactions (default: 30 days ago).
71
+ # :end_date - The end Date to which transactions
72
+ # will be collected (default: today).
73
+ # client - The Plaid::Client instance used to connect to the API
74
+ # (default is to use global Plaid client - Plaid.client).
75
+ #
76
+ # Returns a Plaid::User instance.
77
+ def self.create(product, institution, username, password,
78
+ pin: nil, options: nil, client: nil)
79
+ check_product product
80
+
81
+ payload = { username: username, password: password,
82
+ type: institution.to_s }
83
+ payload[:pin] = pin if pin
84
+ payload[:options] = MultiJson.dump(options) if options
85
+
86
+ conn = Connector.new(product, auth: true, client: client)
87
+ resp = conn.post(payload)
88
+
89
+ new product, response: resp, mfa: conn.mfa?, client: client
90
+ end
91
+
92
+ # Public: Get User instance in case user access token is known.
93
+ #
94
+ # No requests are made, but the returned User instance is ready to be
95
+ # used.
96
+ #
97
+ # product - The Symbol product name you want to use, one of
98
+ # Plaid::PRODUCTS (e.g. :info, :connect, etc.).
99
+ # token - The String access token for the user.
100
+ # client - The Plaid::Client instance used to connect to the API
101
+ # (default is to use global Plaid client - Plaid.client).
102
+ #
103
+ # Returns a Plaid::User instance.
104
+ def self.load(product, token, client: nil)
105
+ new check_product(product), access_token: token, client: client
106
+ end
107
+
108
+ # Public: Exchange a Link public_token for an API access_token.
109
+ #
110
+ # public_token - The String Link public_token.
111
+ # account_id - The String account ID.
112
+ # product - The Symbol product name (default: :connect).
113
+ # client - The Plaid::Client instance used to connect to the API
114
+ # (default is to use global Plaid client - Plaid.client).
115
+ #
116
+ # Returns a new User with access token obtained from Plaid and default
117
+ # product set to product.
118
+ def self.exchange_token(public_token, account_id = nil,
119
+ product: :connect, client: nil)
120
+ check_product product
121
+
122
+ payload = { public_token: public_token }
123
+ payload[:account_id] = account_id if account_id
124
+
125
+ response = Connector.new(:exchange_token, auth: true, client: client)
126
+ .post(payload)
127
+ new product, response: response, client: client
128
+ end
129
+
130
+ # Internal: Initialize a User instance.
131
+ #
132
+ # product - The Symbol product name.
133
+ # access_token - The String access token obtained from Plaid.
134
+ # response - The Hash response body to parse.
135
+ # mfa - The Boolean flag indicating that response body
136
+ # - contains an MFA response.
137
+ # client - The Plaid::Client instance used to connect to the API
138
+ # (default is to use global Plaid client - Plaid.client).
139
+ def initialize(product, access_token: nil, response: nil, mfa: nil,
140
+ client: nil)
141
+ @product = product
142
+ @client = client
143
+ @access_token = access_token if access_token
144
+ @mfa_required = mfa
145
+ @accounts = @initial_transactions = @info = @risk = @income = nil
146
+
147
+ parse_response(response) if response
148
+ end
149
+
150
+ # Public: Find out if MFA is required based on last request.
151
+ #
152
+ # After calling e.g. User.create you might need to make an additional
153
+ # authorization step if MFA is required by the financial institution.
154
+ #
155
+ # Returns true if this step is needed, a falsey value otherwise.
156
+ def mfa?
157
+ @mfa_required
158
+ end
159
+
160
+ # Public: Submit MFA information.
161
+ #
162
+ # info - The String with MFA information (default: nil).
163
+ # send_method - The Hash with code send method information.
164
+ # E.g. { type: 'phone' } or { mask: '123-...-4321' }.
165
+ # Default is first available email.
166
+ #
167
+ # Returns true if whole MFA process is completed, false otherwise.
168
+ def mfa_step(info = nil, send_method: nil)
169
+ payload = { access_token: access_token }
170
+ payload[:mfa] = info if info
171
+ payload[:send_method] = MultiJson.dump(send_method) if send_method
172
+
173
+ conn = Connector.new(product, :step, auth: true)
174
+ response = conn.post(payload)
175
+
176
+ @mfa_required = conn.mfa?
177
+ parse_response(response)
178
+ end
179
+
180
+ # Public: Get transactions.
181
+ #
182
+ # Does a /connect/get call. Updates self.accounts with latest information.
183
+ #
184
+ # pending - the Boolean flag requesting to return pending transactions.
185
+ # account_id - the String Account ID (default: nil). If this argument is
186
+ # present, only transactions for given account will be
187
+ # requested.
188
+ # start_date - The start Date (inclusive).
189
+ # end_date - The end Date (inclusive).
190
+ #
191
+ # Returns an Array of Transaction records.
192
+ def transactions(pending: false, account_id: nil,
193
+ start_date: nil, end_date: nil)
194
+ options = { pending: pending }
195
+ options[:account] = account_id if account_id
196
+ options[:gte] = start_date.to_s if start_date
197
+ options[:lte] = end_date.to_s if end_date
198
+
199
+ response = Connector.new(:connect, :get, auth: true, client: client)
200
+ .post(access_token: access_token,
201
+ options: MultiJson.dump(options))
202
+ update_accounts(response)
203
+ build_objects(response['transactions'], Transaction)
204
+ end
205
+
206
+ # Public: Update user credentials.
207
+ #
208
+ # Updates the user credentials for the current product. See
209
+ # User#for_product.
210
+ #
211
+ # username - The String username associated with the financial
212
+ # institution.
213
+ # password - The String password associated with the financial
214
+ # institution.
215
+ # pin - The String PIN number associated with the financial
216
+ # institution (default: nil).
217
+ #
218
+ # Returns self.
219
+ def update(username, password, pin = nil)
220
+ payload = {
221
+ access_token: access_token,
222
+ username: username,
223
+ password: password
224
+ }
225
+
226
+ payload[:pin] = pin if pin
227
+
228
+ parse_response(Connector.new(product, auth: true, client: client)
229
+ .patch(payload))
230
+
231
+ self
232
+ end
233
+
234
+ # Public: Delete the user.
235
+ #
236
+ # Makes a delete request and freezes self to prevent further modifications
237
+ # to the object.
238
+ #
239
+ # Returns self.
240
+ def delete
241
+ Connector.new(product, auth: true, client: client)
242
+ .delete(access_token: access_token)
243
+
244
+ freeze
245
+ end
246
+
247
+ # Public: Upgrade the user.
248
+ #
249
+ # For an existing user that has been added via any of products (:connect,
250
+ # :auth, :income, :info, or :risk), you can upgrade that user to have
251
+ # functionality with other products.
252
+ #
253
+ # Does a POST /upgrade request.
254
+ #
255
+ # See also User#for_product.
256
+ #
257
+ # product - The Symbol product name you are upgrading the user to, one of
258
+ # Plaid::PRODUCTS.
259
+ #
260
+ # Returns another User record with the same access token, but tied to the
261
+ # new product.
262
+ def upgrade(product)
263
+ payload = { access_token: access_token, upgrade_to: product.to_s }
264
+ response = Connector.new(:upgrade, auth: true, client: client)
265
+ .post(payload)
266
+
267
+ User.new product, response: response
268
+ end
269
+
270
+ # Public: Get the current user tied to another product.
271
+ #
272
+ # No API request is made, just the current product is changed.
273
+ #
274
+ # product - The Symbol product you are selecting, one of Plaid::PRODUCTS.
275
+ #
276
+ # See also User#upgrade.
277
+ #
278
+ # Returns a new User instance.
279
+ def for_product(product)
280
+ User.load product, access_token, client: client
281
+ end
282
+
283
+ # Public: Get auth information for the user (routing numbers for accounts).
284
+ #
285
+ # Not only this method returns the new data, but it updates self.accounts as
286
+ # well.
287
+ #
288
+ # The method does a POST /auth/get request.
289
+ #
290
+ # sync - The Boolean flag which, if true, causes auth information to be
291
+ # rerequested from the server. Otherwise cached version is returned,
292
+ # if it exists.
293
+ #
294
+ # Returns an Array of Account with numbers baked in.
295
+ def auth(sync: false)
296
+ if sync || !@accounts || !@accounts[0] || !@accounts[0].numbers
297
+ response = Connector.new(:auth, :get, auth: true, client: client)
298
+ .post(access_token: access_token)
299
+
300
+ update_accounts(response)
301
+ end
302
+
303
+ accounts
304
+ end
305
+
306
+ # Public: Get info for the user.
307
+ #
308
+ # Does a POST /info/get request.
309
+ #
310
+ # sync - The Boolean flag which, if true, causes information to be
311
+ # rerequested from the server. Otherwise cached version is returned,
312
+ # if it exists.
313
+ #
314
+ # Returns a Plaid::Info instance.
315
+ def info(sync: false)
316
+ if sync || !@info
317
+ parse_response(Connector.new(:info, :get, auth: true, client: client)
318
+ .post(access_token: access_token))
319
+ end
320
+
321
+ @info
322
+ end
323
+
324
+ # Public: Get income information for the user.
325
+ #
326
+ # Does a POST /income/get request.
327
+ #
328
+ # sync - The Boolean flag which, if true, causes income information to be
329
+ # rerequested from the server. Otherwise cached version is returned,
330
+ # if it exists.
331
+ #
332
+ # Returns a Plaid::Income instance.
333
+ def income(sync: false)
334
+ if sync || !@income
335
+ parse_response(Connector.new(:income, :get, auth: true, client: client)
336
+ .post(access_token: access_token))
337
+ end
338
+
339
+ @income
340
+ end
341
+
342
+ # Public: Get risk data for the user's accounts.
343
+ #
344
+ # Does a POST /risk/get request.
345
+ #
346
+ # sync - The Boolean flag which, if true, causes risk information to be
347
+ # rerequested from the server. Otherwise cached version is returned,
348
+ # if it exists.
349
+ #
350
+ # Returns an Array of accounts with risk attribute set.
351
+ def risk(sync: false)
352
+ if sync || !@accounts || !@accounts[0] || !@accounts[0].risk
353
+ parse_response(Connector.new(:risk, :get, auth: true, client: client)
354
+ .post(access_token: access_token))
355
+ end
356
+
357
+ @accounts
358
+ end
359
+
360
+ # Public: Get current account balance.
361
+ #
362
+ # Does a POST /balance request.
363
+ #
364
+ # Returns an Array of Plaid::Account.
365
+ def balance
366
+ response = Connector.new(:balance, auth: true, client: client)
367
+ .post(access_token: access_token)
368
+
369
+ update_accounts(response)
370
+ end
371
+
372
+ private
373
+
374
+ # Internal: Validate the product name.
375
+ def self.check_product(product)
376
+ if Plaid::PRODUCTS.include?(product)
377
+ product
378
+ else
379
+ raise ArgumentError, "product (#{product.inspect}) must be one of " \
380
+ "Plaid products (#{Plaid::PRODUCTS.inspect})"
381
+ end
382
+ end
383
+
384
+ private_class_method :check_product
385
+
386
+ # Internal: Set up attributes from Add User response.
387
+ def parse_response(response)
388
+ @access_token = response['access_token']
389
+ return parse_mfa_response(response) if mfa?
390
+
391
+ @mfa_type = @mfa = nil
392
+
393
+ update_accounts(response) if response['accounts']
394
+
395
+ if (trans = response['transactions'])
396
+ @initial_transactions = build_objects(trans, Transaction)
397
+ end
398
+
399
+ if (income = response['income'])
400
+ @income = Plaid::Income.new(income)
401
+ end
402
+
403
+ return unless (i = response['info'])
404
+ @info = Plaid::Info.new(i)
405
+ end
406
+
407
+ # Internal: Parse an MFA response
408
+ def parse_mfa_response(response)
409
+ @mfa_type = response['type'].to_sym
410
+ @mfa = Plaid.symbolize_hash(response['mfa'])
411
+ end
412
+
413
+ # Internal: Convert an array of data into an array of objects, encapsulating
414
+ # that data.
415
+ def build_objects(data, klass)
416
+ data ? data.map { |element| klass.new(element) } : []
417
+ end
418
+
419
+ # Internal: Update account data from the response.
420
+ def update_accounts(response)
421
+ new_accounts = build_objects(response['accounts'], Account)
422
+
423
+ if @accounts
424
+ Account.merge @accounts, new_accounts
425
+ else
426
+ @accounts = new_accounts
427
+ end
428
+ end
429
+ end
430
+ end