plaid-legacy 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ ## Upgrading from 2.x.x to 3.0.0
2
+
3
+ Version 3.0.0 makes `Plaid::Institution` use new `institutions/all` endpoint
4
+ of Plaid API which unites "native" and "long tail" institutions.
5
+ `Plaid::LongTailInstitution` class is removed, its functionality is
6
+ concentrated in `Plaid::Institution`.
7
+
8
+ Use `Plaid::Institution.all` instead of `Plaid::LongTailInstitution.all` (the
9
+ semantics is the same, with added products param).
10
+
11
+ Use `Plaid::Institution.search` instead of `Plaid::LongTailInstitution.search`.
12
+
13
+ Use `Plaid::Institution.search_by_id` instead of `Plaid::LongTailInstitution.get`.
14
+
15
+
16
+ ## Upgrading from 1.x to 2.0.0
17
+
18
+ Make sure you use Ruby 2.0 or higher.
19
+
20
+ Update the `Plaid.config` block:
21
+
22
+ ```ruby
23
+ Plaid.config do |p|
24
+ p.client_id = '<<< Plaid provided client ID >>>' # WAS: customer_id
25
+ p.secret = '<<< Plaid provided secret key >>>' # No change
26
+ p.env = :tartan # or :api for production # WAS: environment_location
27
+ end
28
+ ```
29
+
30
+ Use `Plaid::User.create` instead of `Plaid.add_user` (**NOTE**: parameter order has changed!)
31
+
32
+ Use `Plaid::User.load` instead of `Plaid.set_user` (**NOTE**: parameter order has changed!)
33
+
34
+ Use `Plaid::User.exchange_token` instead of `Plaid.exchange_token` (**NOTE**: parameter list has changed!)
35
+
36
+ Use `Plaid::User.create` or (`.load`) and `Plaid::User#transactions` instead of `Plaid.transactions`.
37
+
38
+ Use `Plaid::Institution.all` and `Plaid::Institution.get` instead of `Plaid.institution`.
39
+
40
+ Use `Plaid::Category.all` and `Plaid::Category.get` instead of `Plaid.category`.
41
+
42
+ `Plaid::Account#institution_type` was renamed to `Plaid::Account#institution`.
43
+
44
+ `Plaid::Transaction#account` was renamed to `Plaid::Transaction#account_id`.
45
+
46
+ `Plaid::Transaction#date` is a Date, not a String object now.
47
+
48
+ `Plaid::Transaction#cat` was removed. Use `Plaid::Transaction#category_hierarchy` and `Plaid::Transaction#category_id` directly.
49
+
50
+ `Plaid::Transaction#category` was renamed to `Plaid::Transaction#category_hierarchy`.
51
+
52
+ `Plaid::Transaction#pending_transaction` was renamed to `Plaid::Transaction#pending_transaction_id`.
53
+
54
+ Use `Plaid::User#mfa_step` instead of `Plaid::User#select_mfa_method` and `Plaid::User#mfa_authentication`.
55
+
56
+ `Plaid::User#permit?` was removed. You don't need this.
57
+
58
+ `Plaid::User.delete_user` was renamed to `Plaid::User.delete`.
59
+
60
+ **NOTE** that Symbols are now consistently used instead of Strings as product names, keys in hashes, etc. Look at the docs, they have all the examples.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'plaid'
5
+ require 'pry'
6
+
7
+ Plaid.config do |p|
8
+ p.env = :tartan
9
+ p.client_id = ENV['PLAID_CLIENT_ID']
10
+ p.secret = ENV['PLAID_SECRET']
11
+ end
12
+
13
+ Pry.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,64 @@
1
+ require 'plaid/version'
2
+ require 'plaid/errors'
3
+ require 'plaid/connector'
4
+ require 'plaid/category'
5
+ require 'plaid/institution'
6
+ require 'plaid/user'
7
+ require 'plaid/transaction'
8
+ require 'plaid/info'
9
+ require 'plaid/income'
10
+ require 'plaid/client'
11
+ require 'plaid/webhook'
12
+
13
+ require 'uri'
14
+
15
+ # Public: The Plaid namespace.
16
+ module Plaid
17
+ # Public: Available Plaid products.
18
+ PRODUCTS = %i(connect auth info income risk).freeze
19
+
20
+ class <<self
21
+ # Public: The default Client.
22
+ attr_accessor :client
23
+
24
+ # Public: The Integer read timeout for requests to Plaid HTTP API.
25
+ # Should be specified in seconds. Default value is 120 (2 minutes).
26
+ attr_accessor :read_timeout
27
+
28
+ # Public: A helper function to ease configuration.
29
+ #
30
+ # Yields self.
31
+ #
32
+ # Examples
33
+ #
34
+ # Plaid.configure do |p|
35
+ # p.client_id = 'Plaid provided client ID here'
36
+ # p.secret = 'Plaid provided secret key here'
37
+ # p.env = :tartan
38
+ # p.read_timeout = 300 # it's 5 minutes, yay!
39
+ # end
40
+ #
41
+ # Returns nothing.
42
+ def config
43
+ client = Client.new
44
+ yield client
45
+ self.client = client
46
+ end
47
+
48
+ # Internal: Symbolize keys (and values) for a hash.
49
+ #
50
+ # hash - The Hash with string keys (or nil).
51
+ # values - The Boolean flag telling the function to symbolize values
52
+ # as well.
53
+ #
54
+ # Returns a Hash with keys.to_sym (or nil if hash is nil).
55
+ def symbolize_hash(hash, values: false)
56
+ return unless hash
57
+ return hash.map { |h| symbolize_hash(h) } if hash.is_a?(Array)
58
+
59
+ hash.each_with_object({}) do |(k, v), memo|
60
+ memo[k.to_sym] = values ? v.to_sym : v
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,144 @@
1
+ require 'plaid/risk'
2
+
3
+ module Plaid
4
+ # Public: Representation of user account data.
5
+ class Account
6
+ # Public: The String unique ID of the account. E.g.
7
+ # "QPO8Jo8vdDHMepg41PBwckXm4KdK1yUdmXOwK".
8
+ attr_reader :id
9
+
10
+ # Public: The String account ID unique to the accounts of a particular
11
+ # access token. E.g. "KdDjmojBERUKx3JkDd9RuxA5EvejA4SENO4AA".
12
+ attr_reader :item_id
13
+
14
+ # Public: The Float value of the current balance for the account.
15
+ attr_reader :current_balance
16
+
17
+ # Public: The Float value of the available balance for the account.
18
+ #
19
+ # The Available Balance is the Current Balance less any outstanding holds
20
+ # or debits that have not yet posted to the account. Note that not all
21
+ # institutions calculate the Available Balance. In the case that Available
22
+ # Balance is unavailable from the institution, Plaid will either return an
23
+ # Available Balance value of null or only return a Current Balance.
24
+ attr_reader :available_balance
25
+
26
+ # Public: The Symbol institution type, e.g. :wells.
27
+ attr_reader :institution
28
+
29
+ # Public: The Hash with additional information pertaining to the account
30
+ # such as the limit, name, or last few digits of the account number. E.g.
31
+ # {"name": "Plaid Savings", "number": "9606" }.
32
+ attr_reader :meta
33
+
34
+ # Public: The Symbol account type. One of :depository, :credit, :loan,
35
+ # :mortgage, :brokerage, and :other.
36
+ attr_reader :type
37
+
38
+ # Public: The String account subtype. E.g. "savings".
39
+ #
40
+ # Read more about subtypes in the Plaid API docs.
41
+ attr_reader :subtype
42
+
43
+ # Public: The Hash with account and routing numbers for the account.
44
+ #
45
+ # This attribute would be nil unless you used Auth product for the user.
46
+ #
47
+ # The Hash contains Symbol keys and String values. E.g.
48
+ # {routing: "021000021", account: "9900009606", wireRouting: "021000021"}.
49
+ attr_reader :numbers
50
+
51
+ # Public: The Risk information associated with the account.
52
+ #
53
+ # This attribute would be nil unless you used Risk product for the user.
54
+ attr_reader :risk
55
+
56
+ def initialize(hash)
57
+ @id = hash['_id']
58
+ @item_id = hash['_item']
59
+ @meta = hash['meta']
60
+ @type = hash['type'].to_sym
61
+ @subtype = hash['subtype']
62
+ @institution = hash['institution_type'].to_sym
63
+
64
+ unless (bal = hash['balance']).nil?
65
+ @available_balance = bal['available']
66
+ @current_balance = bal['current']
67
+ end
68
+
69
+ if (risk = hash['risk'])
70
+ @risk = Plaid::Risk.new(risk)
71
+ end
72
+
73
+ @numbers = Plaid.symbolize_hash(hash['numbers'])
74
+ end
75
+
76
+ # Public: Get a String representation of the account.
77
+ #
78
+ # Returns a String.
79
+ def inspect
80
+ "#<Plaid::Account id=#{id.inspect}, type=#{type.inspect}, " \
81
+ "name=#{name.inspect}, institution=#{institution.inspect}>"
82
+ end
83
+
84
+ # Public: Get a String representation of the account.
85
+ #
86
+ # Returns a String.
87
+ alias to_s inspect
88
+
89
+ # Public: Get the account name.
90
+ #
91
+ # The name is obtained from #meta Hash.
92
+ #
93
+ # Returns the String name.
94
+ def name
95
+ meta && meta['name']
96
+ end
97
+
98
+ # Internal: Merge account information.
99
+ #
100
+ # accounts - The Array of Account instances.
101
+ # new_accounts - The Array of Account instances.
102
+ #
103
+ # Returns accounts.
104
+ def self.merge(accounts, new_accounts)
105
+ # Index accounts by their ID.
106
+ #
107
+ # Same as index = accounts.index_by(&:id) in ActiveSupport.
108
+ index = Hash[accounts.map { |a| [a.id, a] }]
109
+
110
+ new_accounts.each do |acc|
111
+ if (old_acc = index[acc.id])
112
+ old_acc.update_from(acc)
113
+ else
114
+ accounts << acc
115
+ end
116
+ end
117
+
118
+ accounts
119
+ end
120
+
121
+ # Internal: Update account information.
122
+ #
123
+ # All fields which are not nil in another are copied to self.
124
+ #
125
+ # another - The Account instance with new information.
126
+ #
127
+ # Returns self.
128
+ def update_from(another)
129
+ # A sanity check. Nobody would want to update information from totally
130
+ # different account!
131
+ if id != another.id
132
+ raise ArgumentError, 'Plaid::Account#update_from: id != another.id!'
133
+ end
134
+
135
+ %i(item_id meta name type subtype institution available_balance
136
+ current_balance numbers risk).each do |field|
137
+ value = another.send(field)
138
+ instance_variable_set("@#{field}", value) unless value.nil?
139
+ end
140
+
141
+ self
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,62 @@
1
+ module Plaid
2
+ # Public: A class which encapsulates information about a Plaid category.
3
+ class Category
4
+ # Public: The String category ID, e.g. "21010006".
5
+ attr_reader :id
6
+
7
+ # Public: The Symbol category type. One of :special, :place, :digital.
8
+ attr_reader :type
9
+
10
+ # Public: The Array of String hierarchy. E.g.
11
+ # ["Food and Drink", "Nightlife"].
12
+ attr_reader :hierarchy
13
+
14
+ # Internal: Initialize a Category with given fields.
15
+ def initialize(fields)
16
+ @type = fields['type'].to_sym
17
+ @hierarchy = fields['hierarchy']
18
+ @id = fields['id']
19
+ end
20
+
21
+ # Public: Get a String representation of Category.
22
+ #
23
+ # Returns a String.
24
+ def inspect
25
+ "#<Plaid::Category id=#{id.inspect}, type=#{type.inspect}, " \
26
+ "hierarchy=#{hierarchy.inspect}>"
27
+ end
28
+
29
+ # Public: Get a String representation of Category.
30
+ #
31
+ # Returns a String.
32
+ alias to_s inspect
33
+
34
+ # Public: Get information about all available categories.
35
+ #
36
+ # Does a GET /categories call.
37
+ #
38
+ # client - The Plaid::Client instance used to connect
39
+ # (default: Plaid.client).
40
+ #
41
+ # Returns an Array of Category instances.
42
+ def self.all(client: nil)
43
+ Connector.new(:categories, client: client).get.map do |category_data|
44
+ new(category_data)
45
+ end
46
+ end
47
+
48
+ # Public: Get information about a given category.
49
+ #
50
+ # Does a GET /categories/:id call.
51
+ #
52
+ # id - the String category ID (e.g. "17001013").
53
+ # client - The Plaid::Client instance used to connect
54
+ # (default: Plaid.client).
55
+ #
56
+ # Returns a Category instance or raises Plaid::NotFoundError if category
57
+ # with given id is not found.
58
+ def self.get(id, client: nil)
59
+ new Connector.new(:categories, id, client: client).get
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,69 @@
1
+ module Plaid
2
+ # Public: A class encapsulating client_id, secret, and Plaid API URL.
3
+ class Client
4
+ # Public: The String Plaid account client ID to authenticate requests.
5
+ attr_accessor :client_id
6
+
7
+ # Public: The String Plaid account secret to authenticate requests.
8
+ attr_accessor :secret
9
+
10
+ # Public: Plaid environment, i.e. String base URL of the API site.
11
+ #
12
+ # E.g. 'https://tartan.plaid.com'.
13
+ attr_reader :env
14
+
15
+ # Public: Set Plaid environment to use.
16
+ #
17
+ # env - The Symbol (:tartan, :production), or a full String URL like
18
+ # 'https://tartan.plaid.com'.
19
+ def env=(env)
20
+ case env
21
+ when :tartan
22
+ @env = 'https://tartan.plaid.com/'
23
+ when :production
24
+ @env = 'https://api.plaid.com/'
25
+ when String
26
+ begin
27
+ URI.parse(env)
28
+ @env = env
29
+ rescue
30
+ raise ArgumentError, 'Invalid URL in Plaid::Client.env' \
31
+ " (#{env.inspect}). " \
32
+ 'Specify either Symbol (:tartan, :production),' \
33
+ " or a full URL, like 'https://tartan.plaid.com'"
34
+ end
35
+ else
36
+ raise ArgumentError, 'Invalid value for Plaid::Client.env' \
37
+ " (#{env.inspect}): " \
38
+ 'must be :tartan, :production, or a full URL, ' \
39
+ "e.g. 'https://tartan.plaid.com'"
40
+ end
41
+ end
42
+
43
+ # Public: Construct a Client instance.
44
+ #
45
+ # env - The Symbol (:tartan, :production), or a full String URL like
46
+ # 'https://tartan.plaid.com'.
47
+ # client_id - The String Plaid account client ID to authenticate requests.
48
+ # secret - The String Plaid account secret to authenticate requests.
49
+ def initialize(env: nil, client_id: nil, secret: nil)
50
+ env && self.env = env
51
+ self.client_id = client_id
52
+ self.secret = secret
53
+ end
54
+
55
+ # Public: Check if client_id is configured.
56
+ #
57
+ # Returns true if it is.
58
+ def client_id_configured?
59
+ @client_id.is_a?(String) && !@client_id.empty?
60
+ end
61
+
62
+ # Public: Check if client_id is configured.
63
+ #
64
+ # Returns true if it is.
65
+ def secret_configured?
66
+ @secret.is_a?(String) && !@secret.empty?
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,172 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'multi_json'
4
+
5
+ module Plaid
6
+ # Internal: A class encapsulating HTTP requests to the Plaid API servers
7
+ class Connector
8
+ attr_reader :uri, :http, :request, :response, :body
9
+
10
+ # Internal: Default read timeout for HTTP calls.
11
+ DEFAULT_TIMEOUT = 120
12
+
13
+ # Internal: Prepare to run request.
14
+ #
15
+ # path - The String path without leading slash. E.g. 'connect'
16
+ # subpath - The String subpath. E.g. 'get'
17
+ # auth - The Boolean flag indicating that client_id and secret should be
18
+ # included into the request payload.
19
+ # client - The Plaid::Client instance used to connect
20
+ # (default: Plaid.client).
21
+ def initialize(path, subpath = nil, auth: false, client: nil)
22
+ @auth = auth
23
+ @client = client || Plaid.client
24
+ verify_configuration
25
+
26
+ path = File.join(@client.env, path.to_s)
27
+ path = File.join(path, subpath.to_s) if subpath
28
+
29
+ @uri = URI.parse(path)
30
+
31
+ @http = Net::HTTP.new(@uri.host, @uri.port)
32
+ @http.use_ssl = true
33
+
34
+ @http.read_timeout = Plaid.read_timeout || DEFAULT_TIMEOUT
35
+ end
36
+
37
+ # Internal: Run GET request.
38
+ #
39
+ # Returns the parsed JSON response body.
40
+ def get(payload = {})
41
+ payload = with_credentials(payload)
42
+
43
+ @uri.query = URI.encode_www_form(payload) unless payload.empty?
44
+
45
+ run Net::HTTP::Get.new(@uri)
46
+ end
47
+
48
+ # Internal: Run POST request.
49
+ #
50
+ # Adds client_id and secret to the payload if @auth is true.
51
+ #
52
+ # payload - The Hash with data.
53
+ #
54
+ # Returns the parsed JSON response body.
55
+ def post(payload)
56
+ post_like payload, Net::HTTP::Post.new(@uri.path)
57
+ end
58
+
59
+ # Internal: Run PATCH request.
60
+ #
61
+ # Adds client_id and secret to the payload if @auth is true.
62
+ #
63
+ # payload - The Hash with data.
64
+ #
65
+ # Returns the parsed JSON response body.
66
+ def patch(payload)
67
+ post_like payload, Net::HTTP::Patch.new(@uri.path)
68
+ end
69
+
70
+ # Internal: Run DELETE request.
71
+ #
72
+ # Adds client_id and secret to the payload if @auth is true.
73
+ #
74
+ # payload - The Hash with data.
75
+ #
76
+ # Returns the parsed JSON response body.
77
+ def delete(payload)
78
+ post_like payload, Net::HTTP::Delete.new(@uri.path)
79
+ end
80
+
81
+ # Internal: Check if MFA response received.
82
+ #
83
+ # Returns true if response has code 201.
84
+ def mfa?
85
+ @response.is_a?(Net::HTTPCreated)
86
+ end
87
+
88
+ private
89
+
90
+ # Internal: Run the request and process the response.
91
+ #
92
+ # Returns the parsed JSON body or raises an appropriate exception (a
93
+ # descendant of Plaid::PlaidError).
94
+ def run(request)
95
+ @request = request
96
+ @response = http.request(@request)
97
+
98
+ if @response.body.nil? || @response.body.empty?
99
+ raise Plaid::ServerError.new(0, 'Server error', 'Try to connect later')
100
+ end
101
+
102
+ # All responses are expected to have a JSON body, so we always parse,
103
+ # not looking at the status code.
104
+ @body = MultiJson.load(@response.body)
105
+
106
+ case @response
107
+ when Net::HTTPSuccess, Net::HTTPCreated
108
+ @body
109
+ else
110
+ raise_error
111
+ end
112
+ end
113
+
114
+ # Internal: Run POST-like request.
115
+ #
116
+ # payload - The Hash with posted data.
117
+ # request - The Net::HTTPGenericRequest descendant instance.
118
+ def post_like(payload, request)
119
+ request.set_form_data(with_credentials(payload))
120
+
121
+ run request
122
+ end
123
+
124
+ # Internal: Raise an error with the class depending on @response.
125
+ #
126
+ # Returns an Array with arguments.
127
+ def raise_error
128
+ klass = case @response
129
+ when Net::HTTPBadRequest then Plaid::BadRequestError
130
+ when Net::HTTPUnauthorized then Plaid::UnauthorizedError
131
+ when Net::HTTPPaymentRequired then Plaid::RequestFailedError
132
+ when Net::HTTPNotFound then Plaid::NotFoundError
133
+ else
134
+ Plaid::ServerError
135
+ end
136
+
137
+ raise klass.new(body['code'], body['message'], body['resolve'])
138
+ end
139
+
140
+ # Internal: Verify that Plaid environment is properly configured.
141
+ #
142
+ # Raises NotConfiguredError if anything is wrong.
143
+ def verify_configuration
144
+ raise_not_configured(:env, auth: false) unless @client.env
145
+ return unless @auth
146
+
147
+ !@client.client_id_configured? && raise_not_configured(:client_id)
148
+ !@client.secret_configured? && raise_not_configured(:secret)
149
+ end
150
+
151
+ # Internal: Raise a NotConfiguredError exception with proper message.
152
+ def raise_not_configured(field, auth: true)
153
+ message = "You must set Plaid::Client.#{field} before using any methods"
154
+ message << ' which require authentication' if auth
155
+ message << "! It's current value is #{@client.send(field).inspect}. " \
156
+ 'E.g. add a Plaid.config do .. end block somewhere in the ' \
157
+ 'initialization code of your program.'
158
+
159
+ raise NotConfiguredError, message
160
+ end
161
+
162
+ # Internal: Merge credentials to the payload if needed.
163
+ def with_credentials(payload)
164
+ if @auth
165
+ payload.merge(client_id: @client.client_id,
166
+ secret: @client.secret)
167
+ else
168
+ payload
169
+ end
170
+ end
171
+ end
172
+ end