plaid 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8580b88cd18ddfec32569abeef15135a37209049
4
- data.tar.gz: 4b23dd43db4013e15a0d78777e7b751c2918c5bc
3
+ metadata.gz: 3b8b2f76594f65f973077ad6cde5d686bf214e0f
4
+ data.tar.gz: f776703e98bdd32da28c1451c9686490eca81767
5
5
  SHA512:
6
- metadata.gz: df9a31e7100ca2418f0ebdf469ea42630a8c3d0ed29fc1373efe5790ad2da25aebbeb2e39f10d4abd4ae892d787678026e704cbbac60bdd84d08777a95c3ea9c
7
- data.tar.gz: 00cc4839fe0524bfbe90d15121bbfee0537ab0c4dbfb73c5c6c8334d32e3b808f6c88efeea46d9029739a21b23a06b3830fb8a0add17360874b538b8ef35d618
6
+ metadata.gz: e9518764f519d2f8cc5535e50a16aed204032cb39e9e700c77c58782548dce249af9a76ab306aaa824023db281c3859449d4c47fb576c1da5abd8fc24a6c926c
7
+ data.tar.gz: 0f378b719b774502fb0f88dda4b82c66e62921e7d16b58ffb01300d1a2e6e8d524231b4b98f3ca8c26bdc11c0937636300444d62e0392887ed6e0320b0647077
data/README.md CHANGED
@@ -6,7 +6,7 @@ Ruby bindings for the Plaid API
6
6
 
7
7
  This version is a beta version that contains failing tests for the new 'info' endpoint. While these have been tested individually on real accounts the tests here will fail with the test accounts supplied. These will be updated soon with test credentials.
8
8
 
9
- Latest stable version: **1.4.3**
9
+ Latest stable version: **1.5.0**
10
10
 
11
11
  This version removes the need to use 'type' in each additional call.
12
12
 
@@ -32,6 +32,7 @@ Or install it system wide as:
32
32
 
33
33
  Please read the great documentation at http://plaid.com/docs/ for more information.
34
34
 
35
+ ### Configuring Plaid
35
36
  Configure the gem with your customer id, secret key, and the environment path you would like to use.
36
37
 
37
38
  ```ruby
@@ -39,15 +40,78 @@ Plaid.config do |p|
39
40
  p.customer_id = 'Plaid provided customer ID here'
40
41
  p.secret = 'Plaid provided secret key here'
41
42
  p.environment_location = 'URL for the development or production environment'
43
+ # i.e. 'https://tartan.plaid.com/' for development, or
44
+ # 'https://api.plaid.com/' for production
42
45
  end
43
46
  ```
44
47
 
48
+ ### Creating a new Plaid User
45
49
  Authenticate a user to your desired level of api access (auth / connect).
46
50
 
47
51
  ```ruby
48
52
  user = Plaid.add_user('auth','plaid_test','plaid_good','wells')
49
53
  ```
50
54
 
55
+ If the authentication requires a pin, you can pass it in as the fifth argument:
56
+
57
+ ```ruby
58
+ user = Plaid.add_user('auth', 'plaid_test', 'plaid_good', 'usaa', '1234')
59
+ ```
60
+
61
+ To add options such as `login_only` or `webhooks`, use the sixth argument:
62
+
63
+ ```ruby
64
+ user = Plaid.add_user('auth','plaid_test','plaid_good','wells', nil, { login_only: true, webhooks: 'https://example.org/callbacks/plaid')
65
+ ```
66
+
67
+ ### Restoring a Plaid User using an access token
68
+
69
+ ```ruby
70
+ user = Plaid.set_user('access_token')
71
+ ```
72
+
73
+ ```ruby
74
+ user = Plaid.set_user('access_token', 'wells')
75
+ ```
76
+
77
+ ### Exchanging a Link public_token for a Plaid access_token
78
+
79
+ ```ruby
80
+ Plaid.config do |p|
81
+ p.customer_id = 'test_id'
82
+ p.secret = 'test_secret'
83
+ p.environment_location = 'https://tartan.plaid.com'
84
+ end
85
+
86
+ # Exchange a Link public_token for a Plaid access_token
87
+ exchangeTokenResponse = Plaid.exchange_token('test,chase,connected')
88
+
89
+ # Use the API access_token to initialize a user
90
+ # Note: This example assumes you are using Link with the "auth" product
91
+ user = Plaid.set_user(exchangeTokenResponse.access_token, ['auth'])
92
+
93
+ # Retrieve the user's accounts
94
+ user.get('auth')
95
+
96
+ # Print the name of each account
97
+ user.accounts.each { |account| print account.meta['name'] + "\n"}
98
+ ```
99
+
100
+ ## Semantic Versioning
101
+
102
+ Methods marked with `API: public` are officially supported by the gem maintainers. Since
103
+ we are using semantic versioning (http://semver.org/spec/v2.0.0.html), the maintainers are
104
+ commited to backwards-compatibility support for these API calls when we update the Minor
105
+ version. So for example, going from version 1.4.x to 1.5.x will not change these public
106
+ API calls.
107
+
108
+ However, we may change these method signatures or even the gem architecture when updating
109
+ the Major number. For example, we have some breaking changes in mind with version 2.0
110
+
111
+ Methods marked with `API: semi-private` are used internally for consistency. While it is
112
+ possible to monkey-patch against them for your own use, the maintainers make no gaurantees
113
+ on backwards compatibility.
114
+
51
115
  ## Learn More
52
116
 
53
117
  Learn about the full functionality of the library on our [Wiki](https://github.com/plaid/plaid-ruby/wiki)
@@ -56,4 +120,4 @@ Learn about the full functionality of the library on our [Wiki](https://github.c
56
120
 
57
121
  We highly encourage helping out with the gem. Either adding more tests, building new helper classes, fixing bugs, or anything to increase overall quality.
58
122
 
59
- Learn more about best practices, submitting a pull request, and rules for the build on our [Wiki](https://github.com/plaid/plaid-ruby/wiki/Contribute!)
123
+ Learn more about best practices, submitting a pull request, and rules for the build on our [Wiki](https://github.com/plaid/plaid-ruby/wiki/Contribute!)
data/lib/plaid/config.rb CHANGED
@@ -1,13 +1,19 @@
1
1
  module Plaid
2
2
  module Configure
3
- attr_writer :customer_id, :secret, :environment_location
3
+ attr_accessor :customer_id, :secret, :environment_location
4
4
 
5
5
  KEYS = [:customer_id, :secret, :environment_location]
6
6
 
7
7
  def config
8
8
  yield self
9
+
10
+ # Add trailing slash to api url if it is not present
11
+ if self.environment_location[-1] != '/'
12
+ self.environment_location += '/'
13
+ end
14
+
9
15
  self
10
16
  end
11
17
 
12
18
  end
13
- end
19
+ end
@@ -0,0 +1,121 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+ module Plaid
5
+ class Connection
6
+ class << self
7
+ # API: semi-private
8
+ def post(path, options = {})
9
+ uri = build_uri(path)
10
+ options.merge!(client_id: Plaid.customer_id, secret: Plaid.secret)
11
+ res = Net::HTTP.post_form(uri,options)
12
+ parse_response(res)
13
+ end
14
+
15
+ # API: semi-private
16
+ def get(path, id = nil)
17
+ uri = build_uri(path,id)
18
+ res = Net::HTTP.get(uri)
19
+ parse_get_response(res)
20
+ end
21
+
22
+ # API: semi-private
23
+ def secure_get(path, access_token, options = {})
24
+ uri = build_uri(path)
25
+ options.merge!({access_token:access_token})
26
+ req = Net::HTTP::Get.new(uri)
27
+ req.body = URI.encode_www_form(options) if options
28
+ req.content_type = 'multipart/form-data'
29
+ res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request(req) }
30
+ parse_response(res)
31
+ end
32
+
33
+ # API: semi-private
34
+ def patch(path, options = {})
35
+ uri = build_uri(path)
36
+ options.merge!(client_id: Plaid.customer_id, secret: Plaid.secret)
37
+ req = Net::HTTP::Patch.new(uri)
38
+ req.body = URI.encode_www_form(options) if options
39
+ req.content_type = 'multipart/form-data'
40
+ res = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request(req) }
41
+ parse_response(res)
42
+ end
43
+
44
+ # API: semi-private
45
+ def delete(path, options = {})
46
+ uri = build_uri(path)
47
+ options.merge!(client_id: Plaid.customer_id, secret: Plaid.secret)
48
+ req = Net::HTTP::Delete.new(uri)
49
+ req.body = URI.encode_www_form(options) if options
50
+ req.content_type = 'multipart/form-data'
51
+ Net::HTTP.start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') { |http| http.request(req) }
52
+ end
53
+
54
+ # API: semi-private
55
+ # TODO: Refactor these to use symbols instead of strings
56
+ def error_handler(err, res = nil)
57
+ case err
58
+ when 'Bad Request'
59
+ puts res.body
60
+ raise 'The request was malformed. Did you check the API docs?'
61
+ when 'Unauthorized'
62
+ raise 'Access denied: Try using the correct credentials.'
63
+ when 'Corrupted token'
64
+ raise 'It appears that the access token has been corrupted'
65
+ else
66
+ raise err
67
+ end
68
+ end
69
+
70
+ protected
71
+
72
+ # API: semi-private
73
+ def build_uri(path, option = nil)
74
+ path = path + '/' + option unless option.nil?
75
+ URI.parse(Plaid.environment_location + path)
76
+ end
77
+
78
+ private
79
+
80
+ def parse_response(res)
81
+ body = JSON.parse(res.body)
82
+ case res.code.delete('.').to_i
83
+ when 200 then body
84
+ when 201 then { msg: 'Requires further authentication', body: body}
85
+ when 400 then error_handler('Bad Request',res)
86
+ when 401
87
+ case body['code']
88
+ when 1108 then error_handler('Institution not supported', res)
89
+ when 1105 then error_handler('Corrupted token', res)
90
+ when 1106 then error_handler('Corrupted public_token', res)
91
+ when 1107 then error_handler('Missing public_token', res)
92
+ when 1501 then error_handler('Not Found', res)
93
+ else error_handler('Unauthorized',res)
94
+ end
95
+ when 402
96
+ return {msg: 'User account is locked', body: body} if body['code'] == 1205
97
+ error_handler('Request Failed', res)
98
+ when 404
99
+ error_handler('Not Found',res)
100
+ else
101
+ error_handler('Server Error',res)
102
+ end
103
+ end
104
+
105
+ def parse_get_response(res)
106
+ body = JSON.parse(res)
107
+ return body if body.kind_of?(Array)
108
+
109
+ case body['code']
110
+ when nil
111
+ body
112
+ when 1301, 1401, 1501, 1601
113
+ error_handler('Not Found',body)
114
+ else
115
+ body
116
+ end
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,24 @@
1
+ module Plaid
2
+ class Account
3
+ attr_accessor :available_balance, :current_balance, :institution_type, :meta, :transactions, :numbers, :name, :id, :type, :subtype
4
+
5
+ def initialize(hash)
6
+ @id = hash['_id']
7
+ @name = hash['name']
8
+ @type = hash['type']
9
+ @meta = hash['meta']
10
+ @institution_type = hash['institution_type']
11
+
12
+ if hash['balance']
13
+ @available_balance = hash['balance']['available']
14
+ @current_balance = hash['balance']['current']
15
+ end
16
+
17
+ # Depository account only, "checkings" or "savings"
18
+ # Available on live data, but not on the test data
19
+ @subtype = hash['subtype']
20
+
21
+ @numbers = hash['numbers'] ? hash['numbers'] : 'Upgrade user to access routing information for this account'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module Plaid
2
+ class Category
3
+ attr_accessor :type, :hierarchy, :id
4
+
5
+ def initialize(fields = {})
6
+ @type = fields['type']
7
+ @hierarchy = fields['hierarchy']
8
+ @id = fields['id']
9
+ end
10
+
11
+ # API: semi-private
12
+ # This method takes an array returned from the API and instantiates all of the categories
13
+ def self.all(res)
14
+ res.map { |cat| new(cat) }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,9 @@
1
+ module Plaid
2
+ class ExchangeTokenResponse
3
+ attr_accessor :access_token
4
+
5
+ def initialize(fields = {})
6
+ @access_token = fields['access_token']
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module Plaid
2
+ class Information
3
+ attr_accessor :names, :emails, :phone_numbers, :addresses
4
+
5
+ def initialize(hash)
6
+ @names = hash['names']
7
+ @emails = hash['emails']
8
+ @phone_numbers = hash['phone_numbers']
9
+ @addresses = hash['addresses']
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ module Plaid
2
+ class Institution
3
+ attr_accessor :id, :name, :type, :has_mfa, :mfa
4
+
5
+ def initialize(fields = {})
6
+ @id = fields['id']
7
+ @name = fields['name']
8
+ @type = fields['type']
9
+ @has_mfa = fields['has_mfa']
10
+ @mfa = fields['mfa']
11
+ end
12
+
13
+ # API: semi-private
14
+ # This method takes an array returned from the API and instantiates all of the institutions
15
+ def self.all(res)
16
+ res.map { |inst| new(inst) }
17
+ end
18
+ end
19
+ end
20
+
@@ -0,0 +1,23 @@
1
+ module Plaid
2
+ class Transaction
3
+ attr_accessor :id, :account, :date, :amount, :name, :meta, :location, :pending, :score, :cat, :type, :category, :category_id
4
+
5
+ def initialize(fields = {})
6
+ @id = fields['_id']
7
+ @account = fields['_account']
8
+ @date = fields['date']
9
+ @amount = fields['amount']
10
+ @name = fields['name']
11
+ @location = fields['meta'].nil? ? {} : fields['meta']['location']
12
+ @pending = fields['pending']
13
+ @score = fields['score']
14
+ @cat = Category.new({ 'id' => fields['category_id'], 'hierarchy' => fields['category'], 'type' => fields['type'] })
15
+
16
+ # Here for backwards compatibility only.
17
+ @type = fields['type']
18
+ @category = fields['category']
19
+ @category_id = fields['category_id']
20
+ @meta = fields['meta']
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,192 @@
1
+ require_relative 'account'
2
+ require_relative 'transaction'
3
+ require_relative 'info'
4
+ require 'json'
5
+
6
+ module Plaid
7
+ class User
8
+ attr_accessor :accounts, :transactions, :access_token, :type, :permissions, :api_res, :pending_mfa_questions, :info, :information
9
+
10
+ # API: public
11
+ # Use this method to select the MFA method
12
+ def select_mfa_method(selection, type=nil)
13
+ type = self.type if type.nil?
14
+ auth_path = self.permissions.last + '/step'
15
+ res = Connection.post(auth_path, { options: { send_method: selection }.to_json, access_token: self.access_token, type: type })
16
+ update(res, self.permissions.last)
17
+ end
18
+
19
+ # API: public
20
+ # Use this method to send back the MFA code or answer
21
+ def mfa_authentication(auth, type = nil)
22
+ type = self.type if type.nil?
23
+ auth_path = self.permissions.last + '/step'
24
+ res = Connection.post(auth_path, { mfa: auth, access_token: self.access_token, type: type })
25
+ self.accounts = []
26
+ self.transactions = []
27
+ update(res)
28
+ end
29
+
30
+ # API: public
31
+ # Use this method to find out API levels available for this user
32
+ def permit?(auth_level)
33
+ self.permissions.include? auth_level
34
+ end
35
+
36
+ # API: public
37
+ # Use this method to upgrade a user to another api level
38
+ def upgrade(api_level=nil)
39
+ if api_level.nil?
40
+ api_level = 'auth' unless self.permit? 'auth'
41
+ api_level = 'connect' unless self.permit? 'connect'
42
+ end
43
+ res = Connection.post('upgrade', { access_token: self.access_token, upgrade_to: api_level })
44
+
45
+ # Reset accounts and transaction
46
+ self.accounts = []
47
+ self.transactions = []
48
+ update(res)
49
+ end
50
+
51
+ # API: public
52
+ # Use this method to delete a user from the Plaid API
53
+ def delete_user
54
+ Connection.delete('info', { access_token: self.access_token })
55
+ end
56
+
57
+ ### Internal build methods
58
+ def initialize
59
+ self.accounts = []
60
+ self.transactions = []
61
+ self.permissions = []
62
+ self.access_token = ''
63
+ self.api_res = ''
64
+ self.info = {}
65
+ end
66
+
67
+ # API: semi-private
68
+ # This class method instantiates a new Account object and updates it with the results
69
+ # from the API
70
+ def self.build(res, api_level = nil)
71
+ self.new.update(res, api_level)
72
+ end
73
+
74
+ # API: semi-private
75
+ # This method updates Account with the results returned from the API
76
+ def update(res, api_level = nil)
77
+ self.permit! api_level
78
+
79
+ if res[:msg].nil?
80
+ populate_user!(res)
81
+ clean_up_user!
82
+ else
83
+ set_mfa_request!(res)
84
+ end
85
+
86
+ return self
87
+ end
88
+
89
+ # Internal helper methods
90
+
91
+ # API: semi-private
92
+ # Internal helper method to set the available API levels
93
+ def permit!(api_level)
94
+ return if api_level.nil? || self.permit?(api_level)
95
+ self.permissions << api_level
96
+ end
97
+
98
+ # API: semi-private
99
+ # Gets auth, connect, or info of the user
100
+ # TODO: (2.0) auth_level should be symbols instead of string
101
+ def get(auth_level, options = {})
102
+ return false unless self.permit? auth_level
103
+ case auth_level
104
+ when 'auth'
105
+ update(Connection.post('auth/get', access_token: self.access_token))
106
+ when 'connect'
107
+ payload = { access_token: self.access_token }.merge(options)
108
+ update(Connection.post('connect/get', payload))
109
+ when 'info'
110
+ update(Connection.secure_get('info', self.access_token))
111
+ else
112
+ raise "Invalid auth level: #{auth_level}"
113
+ end
114
+ end
115
+
116
+ # API: semi-private
117
+ def get_auth
118
+ get('auth')
119
+ end
120
+
121
+ # API: semi-private
122
+ def get_connect(options={})
123
+ get('connect', options)
124
+ end
125
+
126
+ # API: semi-private
127
+ def get_info
128
+ get('info')
129
+ end
130
+
131
+ # API: semi-private
132
+ # Helper method to update user information
133
+ # Requires 'info' api level
134
+ def update_info(username,pass,pin=nil)
135
+ return false unless self.permit? 'info'
136
+
137
+ payload = { username: username, password: pass, access_token: self.access_token }
138
+ payload.merge!(pin: pin) if pin
139
+ update(Plaid.patch('info', payload))
140
+ end
141
+
142
+ # API: semi-private
143
+ # Helper method to update user balance
144
+ def update_balance
145
+ update(Connection.post('balance', { access_token: self.access_token }))
146
+ end
147
+
148
+ private
149
+
150
+ def clean_up_user!
151
+ self.accounts.select! { |c| c.instance_of? Account }
152
+ end
153
+
154
+ def set_mfa_request!(res)
155
+ self.access_token = res[:body]['access_token']
156
+ self.pending_mfa_questions = res[:body]
157
+ self.api_res = res[:msg]
158
+ end
159
+
160
+ def populate_user!(res)
161
+ res['accounts'].each do |account|
162
+ if self.accounts.any? { |h| h == account['_id'] }
163
+ owned_account = self.accounts.find { |h| h == account['_id'] }
164
+ owned_account.new(account)
165
+ else
166
+ self.accounts << Account.new(account)
167
+ end
168
+ end if res['accounts']
169
+
170
+ res['transactions'].each do |transaction|
171
+ if self.transactions.any? { |t| t == transaction['_id'] }
172
+ owned_transaction = self.transactions.find { |h| h == transaction['_id'] }
173
+ owned_transaction.new(transaction)
174
+ else
175
+ self.transactions << Transaction.new(transaction)
176
+ end
177
+ end if res['transactions']
178
+
179
+ self.pending_mfa_questions = ''
180
+ self.information = Information.new(res['info']) if res['info']
181
+ self.api_res = 'success'
182
+
183
+ # TODO: Remove the following line when upgrading to V-2
184
+ self.info.merge!(res['info']) if res['info']
185
+ # End TODO
186
+
187
+ self.access_token = res['access_token'].split[0]
188
+ self.type = res['access_token'].split[1]
189
+ end
190
+
191
+ end
192
+ end
data/lib/plaid/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Plaid
2
- VERSION = '1.4.3'
2
+ VERSION = '1.5.0'
3
3
  end