old_plaid 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ module OldPlaid
2
+ class OldPlaidError < StandardError
3
+ attr_reader :code
4
+ attr_reader :resolve
5
+
6
+ def initialize(code, message, resolve)
7
+ super(message)
8
+ @code = code
9
+ @resolve = resolve
10
+ end
11
+ end
12
+
13
+ class BadRequest < OldPlaidError
14
+ end
15
+
16
+ class Unauthorized < OldPlaidError
17
+ end
18
+
19
+ class RequestFailed < OldPlaidError
20
+ end
21
+
22
+ class NotFound < OldPlaidError
23
+ end
24
+
25
+ class ServerError < OldPlaidError
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ module OldPlaid
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['meta']['name'] if hash['meta']
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 OldPlaid
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,11 @@
1
+ module OldPlaid
2
+ class ExchangeTokenResponse
3
+ attr_accessor :access_token
4
+ attr_accessor :stripe_bank_account_token
5
+
6
+ def initialize(fields = {})
7
+ @access_token = fields['access_token']
8
+ @stripe_bank_account_token = fields['stripe_bank_account_token']
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module OldPlaid
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,22 @@
1
+ module OldPlaid
2
+ class Institution
3
+ attr_accessor :id, :name, :type, :has_mfa, :mfa, :credentials, :products
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
+ @credentials = fields['credentials']
12
+ @products = fields['products']
13
+ end
14
+
15
+ # API: semi-private
16
+ # This method takes an array returned from the API and instantiates all of the institutions
17
+ def self.all(res)
18
+ res.map { |inst| new(inst) }
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,24 @@
1
+ module OldPlaid
2
+ class Transaction
3
+ attr_accessor :id, :account, :date, :amount, :name, :meta, :location, :pending, :score, :cat, :type, :category, :category_id, :pending_transaction
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
+ @pending_transaction = fields['_pendingTransaction']
14
+ @score = fields['score']
15
+ @cat = Category.new({ 'id' => fields['category_id'], 'hierarchy' => fields['category'], 'type' => fields['type'] })
16
+
17
+ # Here for backwards compatibility only.
18
+ @type = fields['type']
19
+ @category = fields['category']
20
+ @category_id = fields['category_id']
21
+ @meta = fields['meta']
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,189 @@
1
+ require_relative 'account'
2
+ require_relative 'transaction'
3
+ require_relative 'info'
4
+ require 'json'
5
+
6
+ module OldPlaid
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 OldPlaid 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
+ 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(OldPlaid.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.none? { |h| h.id == account['_id'] }
163
+ self.accounts << Account.new(account)
164
+ end
165
+ end if res['accounts']
166
+
167
+ res['transactions'].each do |transaction|
168
+ if self.transactions.any? { |t| t == transaction['_id'] }
169
+ owned_transaction = self.transactions.find { |h| h == transaction['_id'] }
170
+ owned_transaction.new(transaction)
171
+ else
172
+ self.transactions << Transaction.new(transaction)
173
+ end
174
+ end if res['transactions']
175
+
176
+ self.pending_mfa_questions = {}
177
+ self.information = Information.new(res['info']) if res['info']
178
+ self.api_res = 'success'
179
+
180
+ # TODO: Remove the following line when upgrading to V-2
181
+ self.info.merge!(res['info']) if res['info']
182
+ # End TODO
183
+
184
+ self.access_token = res['access_token'].split[0]
185
+ self.type = res['access_token'].split[1]
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,3 @@
1
+ module OldPlaid
2
+ VERSION = '1.7.1'
3
+ end
data/old_plaid.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'old_plaid/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'old_plaid'
7
+ spec.version = OldPlaid::VERSION
8
+ spec.authors = ['Justin Crites']
9
+ spec.email = ['crites.justin@gmail.com']
10
+ spec.summary = 'Ruby bindings for OldPlaid'
11
+ spec.description = 'Ruby gem wrapper for the OldPlaid API. Read more at the homepage, the wiki, or the plaid documentation.'
12
+ spec.homepage = 'https://github.com/plaid/plaid-ruby'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 1.9.3'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~>3.1'
24
+ spec.add_development_dependency 'pry'
25
+ spec.add_development_dependency 'pry-stack_explorer'
26
+ spec.add_development_dependency 'webmock'
27
+ end
28
+
@@ -0,0 +1,263 @@
1
+ # Authentication flow specs - returns OldPlaid::User
2
+
3
+ describe OldPlaid do
4
+ let(:api_level) { raise "Define let(:api_level)" }
5
+ let(:username) { raise "Define let(:username)" }
6
+ let(:password) { raise "Define let(:password)" }
7
+ let(:type) { raise "Define let(:type)" }
8
+ let(:pin) { nil }
9
+ let(:options) { nil }
10
+
11
+ describe '.add_user' do
12
+ let(:user) { OldPlaid.add_user api_level, username, password, type, pin, options }
13
+
14
+ context 'with correct credentials for single user auth' do
15
+ let(:username) { 'plaid_test' }
16
+ let(:password) { 'plaid_good' }
17
+ let(:type) { 'wells' }
18
+
19
+ context 'and "connect" level of api access' do
20
+ let(:api_level) { 'connect' }
21
+
22
+ it { expect(user.accounts).not_to be_empty }
23
+
24
+ context 'with webhook' do
25
+ let(:options) { { login_only: true, webhook: 'test.com/test.endpoint.aspx' } }
26
+ it { expect(user.accounts).not_to be_empty }
27
+ end
28
+
29
+ context 'when account is locked' do
30
+ let(:password) { 'plaid_locked' }
31
+
32
+ it 'raises a locked error' do
33
+ expect { user }.to raise_error(OldPlaid::RequestFailed) { |error|
34
+ expect(error.code).to eq(1205)
35
+ }
36
+ end
37
+ end
38
+
39
+ context 'with connection options' do
40
+ context 'when requests pending transactions from an institution' do
41
+ let(:options) { { pending: true } }
42
+ it { expect(user.accounts).not_to be_empty }
43
+ end
44
+
45
+ context 'when login only is true' do
46
+ let(:options) { { login_only: true } }
47
+ it { expect(user.accounts).not_to be_empty }
48
+ end
49
+
50
+ context 'sets a start date for transactions' do
51
+ let(:options) { { login_only: true, start_date: '10 days ago'} }
52
+ it { expect(user.accounts).not_to be_empty }
53
+ end
54
+
55
+ context 'sets an end date for transactions' do
56
+ let(:options) { { login_only: true, end_date: '10 days ago'} }
57
+ it { expect(user.accounts).not_to be_empty }
58
+ end
59
+
60
+ context 'sets start and end dates for transactions' do
61
+ let(:options) { { gte: "05/10/2014" , lte: "06/10/2014" } }
62
+ it { expect(user.transactions).not_to be_nil }
63
+ end
64
+
65
+ pending 'with JSON-encoded string for options'
66
+ end
67
+ end
68
+
69
+ context 'and "auth" level of api access' do
70
+ let(:api_level) { 'auth' }
71
+ it { expect(user.accounts.first.numbers).not_to be_empty }
72
+ end
73
+
74
+ context 'and "info" level of api access' do
75
+ let(:api_level) { 'info' }
76
+ it { expect(user.info).not_to be_empty }
77
+ end
78
+ end
79
+
80
+ context 'with incorrect credentials for single factor auth' do
81
+ # Set up correct credentials. Override with bad element
82
+ # within each context block
83
+ let(:username) { 'plaid_test' }
84
+ let(:password) { 'plaid_good' }
85
+ let(:type) { 'wells' }
86
+
87
+ context 'at "auth" level api access' do
88
+ let(:api_level) { 'auth' }
89
+
90
+ context 'using incorrect password' do
91
+ let(:password) { 'plaid_bad' }
92
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
93
+ end
94
+
95
+ context 'using incorrect username' do
96
+ let(:username) { 'plaid_bad' }
97
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
98
+ end
99
+ end
100
+
101
+ context 'at "connect" level api access' do
102
+ let(:api_level) { 'connect' }
103
+
104
+ context 'using incorrect password' do
105
+ let(:password) { 'plaid_bad' }
106
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
107
+ end
108
+
109
+ context 'using incorrect username' do
110
+ let(:username) { 'plaid_bad' }
111
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
112
+ end
113
+ end
114
+
115
+ context 'at "info" level api access' do
116
+ let(:api_level) { 'info' }
117
+
118
+ context 'using incorrect password' do
119
+ let(:password) { 'plaid_bad' }
120
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
121
+ end
122
+
123
+ context 'using incorrect username' do
124
+ let(:username) { 'plaid_bad' }
125
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid credentials') }
126
+ end
127
+ end
128
+ end
129
+
130
+ context 'when institution requires PIN' do
131
+ let(:api_level) { 'connect' }
132
+ let(:username) { 'plaid_test' }
133
+ let(:password) { 'plaid_good' }
134
+ let(:type) { 'usaa' }
135
+
136
+ context 'using correct PIN' do
137
+ let(:pin) { '1234' }
138
+ it { expect(user.api_res).to eq 'Requires further authentication' }
139
+ end
140
+
141
+ context 'using incorrect PIN' do
142
+ let(:pin) { '0000' }
143
+ it { expect { user }.to raise_error(OldPlaid::RequestFailed, 'invalid pin') }
144
+ end
145
+ end
146
+
147
+ context 'when institution requires MFA' do
148
+ let(:api_level) { 'connect' }
149
+ let(:username) { 'plaid_test' }
150
+ let(:password) { 'plaid_good' }
151
+ let(:type) { 'bofa' }
152
+
153
+ context 'with only standard credentials' do
154
+ it { expect(user.api_res).to eq 'Requires further authentication' }
155
+ end
156
+
157
+ context 'with options' do
158
+ context 'with webhook' do
159
+ let(:options) { { login_only: true, webhook: 'test.com/test.endpoint.aspx' } }
160
+ it { expect(user.api_res).to eq 'Requires further authentication' }
161
+ end
162
+
163
+ context 'requests a list of options for code based MFA' do
164
+ let(:type) { 'citi' }
165
+ let(:options) { { list: true } }
166
+
167
+ it { expect(user.pending_mfa_questions).not_to be_nil }
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '.set_user' do
174
+ subject { OldPlaid.set_user(access_token) }
175
+ let(:access_token) { 'test' }
176
+
177
+ it { expect(subject.access_token).to eq(access_token)}
178
+
179
+ context 'gets a valid user with accounts and transactions' do
180
+ let(:user) { OldPlaid.set_user('test_wells',['connect']) }
181
+ it { expect(user.transactions).not_to be_empty }
182
+ end
183
+
184
+ context 'gets a valid user with accounts' do
185
+ let(:user) { OldPlaid.set_user('test_wells',['auth']) }
186
+ it { expect(user.accounts).not_to be_empty }
187
+ end
188
+
189
+ #TODO: Fully vet the info api endpoint for the beta functions before adding this as a supported function.
190
+ pending 'need to vet the info api endpoint' do
191
+ context 'gets a valid user with info' do
192
+ let(:user) { OldPlaid.set_user('test_wells',['info']) }
193
+ it { expect(user.accounts).to be_truthy}
194
+ end
195
+
196
+ context 'gets a fully validated user with all access granted' do
197
+ let(:user) { OldPlaid.set_user('test_wells', ['connect', 'info', 'auth']) }
198
+ it { expect(user.transactions).to be_truthy}
199
+ end
200
+ end
201
+ end
202
+
203
+ describe '.exchange_token' do
204
+ subject { OldPlaid.exchange_token('test,chase,connected', 'QPO8Jo8vdDHMepg41PBwckXm4KdK1yUdmXOwK') }
205
+
206
+ it { expect(subject.access_token).to eql('test_chase') }
207
+ end
208
+
209
+ describe '.transactions' do
210
+ subject { OldPlaid.transactions(access_token, options) }
211
+ let(:access_token) { 'test_wells' }
212
+ let(:options) { nil }
213
+
214
+ context 'without options' do
215
+ it 'returns all accounts' do
216
+ expect(subject.accounts).not_to be_empty
217
+ end
218
+
219
+ it 'returns all transactions' do
220
+ expect(subject.transactions).not_to be_empty
221
+ end
222
+ end
223
+
224
+ context 'when filtering by account' do
225
+ let(:options) { { account: account } }
226
+ let(:account) { 'QPO8Jo8vdDHMepg41PBwckXm4KdK1yUdmXOwK' }
227
+
228
+ it 'returns a subset of transactions' do
229
+ expect(subject.transactions.size).to eql(2)
230
+ end
231
+
232
+ it 'return only transactions from the requested account' do
233
+ expect(subject.transactions.map(&:account).uniq).to eql([account])
234
+ end
235
+ end
236
+
237
+ context 'when filtering by date' do
238
+ let(:options) { { gte: "2014-07-24", lte: "2014-07-25" } }
239
+
240
+ it 'returns a subset of transactions' do
241
+ expect(subject.transactions.size).to eql(1)
242
+ end
243
+
244
+ it 'return only transactions from the requested date range' do
245
+ expect(subject.transactions.map(&:date).uniq).to eql(['2014-07-24'])
246
+ end
247
+ end
248
+
249
+ context 'when filtering by account and date' do
250
+ let(:options) { { account: account , gte: "2014-07-24", lte: "2014-07-25" } }
251
+ let(:account) { 'XARE85EJqKsjxLp6XR8ocg8VakrkXpTXmRdOo' }
252
+
253
+ it 'returns a subset of transactions' do
254
+ expect(subject.transactions.size).to eql(1)
255
+ end
256
+
257
+ it 'returns only transactions from the requested account and date range' do
258
+ expect(subject.transactions.map(&:date).uniq).to eql(['2014-07-24'])
259
+ expect(subject.transactions.map(&:account).uniq).to eql([account])
260
+ end
261
+ end
262
+ end
263
+ end