old_plaid 1.7.1

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.
@@ -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