plaid-legacy 3.0.0

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,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