plaid 1.7.1 → 2.0.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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