increase 0.1.0 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -6
- data/README.md +177 -12
- data/lib/faraday/raise_increase_api_error.rb +11 -0
- data/lib/increase/client.rb +48 -0
- data/lib/increase/configuration.rb +50 -0
- data/lib/increase/errors.rb +99 -0
- data/lib/increase/resource.rb +239 -0
- data/lib/increase/resources/account_numbers.rb +12 -0
- data/lib/increase/resources/account_transfers.rb +13 -0
- data/lib/increase/resources/accounts.rb +13 -0
- data/lib/increase/resources/ach_transfers.rb +13 -0
- data/lib/increase/resources/cards.rb +13 -0
- data/lib/increase/resources/events.rb +10 -0
- data/lib/increase/resources/pending_transactions.rb +10 -0
- data/lib/increase/resources/transactions.rb +10 -0
- data/lib/increase/resources.rb +4 -0
- data/lib/increase/response_hash.rb +15 -0
- data/lib/increase/util.rb +15 -0
- data/lib/increase/version.rb +1 -1
- data/lib/increase/webhook/signature.rb +56 -0
- data/lib/increase.rb +21 -3
- metadata +119 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c94e77f42a4d8c725f5518af02301564c6cbff71f90015063313e983aeac5124
|
4
|
+
data.tar.gz: 1d5ec565ec368dd713b47f18a41d5eb1363e624ef6792c05b87952f9fe368695
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4dd0a71c6eac5742ca4f63237eb8bbccffc8eefe625ce63e345b97cb4431b3cba04744e9acd426665757ab0d1240637a4fb02311cda84cbe7dc8fa1f2657c7bf
|
7
|
+
data.tar.gz: cba66503cbbeaf4fd7bdc6541899d033126a2efbc20a7ce70cb8b14f42a78a30628b3ea40b3da1b7b176e4d8219291d800846061f3004c70c2d0e96f4b548a64
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,35 +1,200 @@
|
|
1
1
|
# Increase
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/increase`. To experiment with that code, run `bin/console` for an interactive prompt.
|
3
|
+
A Ruby API client for [Increase](https://increase.com/), a platform for Bare-Metal Banking APIs!
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
7
|
Install the gem and add to the application's Gemfile by executing:
|
12
8
|
|
13
|
-
|
9
|
+
```sh
|
10
|
+
$ bundle add increase
|
11
|
+
```
|
14
12
|
|
15
13
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
14
|
|
17
|
-
|
15
|
+
```sh
|
16
|
+
$ gem install increase
|
17
|
+
```
|
18
18
|
|
19
19
|
## Usage
|
20
20
|
|
21
|
-
|
21
|
+
```ruby
|
22
|
+
require 'increase'
|
23
|
+
|
24
|
+
# Grab your API key from https://dashboard.increase.com/developers/api_keys
|
25
|
+
Increase.api_key = 'my_api_key'
|
26
|
+
Increase.base_url = 'https://api.increase.com'
|
27
|
+
|
28
|
+
# List transactions
|
29
|
+
Increase::Transactions.list
|
30
|
+
|
31
|
+
# Retrieve a transaction
|
32
|
+
Increase::Transactions.retrieve('transaction_1234abcd')
|
33
|
+
|
34
|
+
# Create an ACH Transfer
|
35
|
+
Increase::AchTransfers.create(
|
36
|
+
account_id: 'account_1234abcd',
|
37
|
+
amount: 100_00, # 10,000 cents ($100 dollars)
|
38
|
+
routing_number: '123456789',
|
39
|
+
account_number: '9876543210',
|
40
|
+
statement_descriptor: 'broke the bank for some retail therapy'
|
41
|
+
)
|
42
|
+
```
|
43
|
+
|
44
|
+
### Per-request Configuration
|
45
|
+
|
46
|
+
By default, the client will use the global API key and configurations. However, you can define a custom client to be
|
47
|
+
used for per-request configuration.
|
48
|
+
|
49
|
+
For example, you may want to have access to production and sandbox data at the same.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
sandbox = Increase::Client.new(
|
53
|
+
api_key: 'time_is_money',
|
54
|
+
base_url: 'https://sandbox.increase.com'
|
55
|
+
)
|
56
|
+
|
57
|
+
# This request will use the `sandbox` client and its configurations
|
58
|
+
Increase::Transactions.with_config(sandbox).list
|
59
|
+
# => [{some sandbox transactions here}, {transaction}, {transaction}]
|
60
|
+
|
61
|
+
# This request will still use the global configurations (where the API key is a production key)
|
62
|
+
Increase::Transactions.list
|
63
|
+
# => [{some production transactions here}, {transaction}, {transaction}]
|
64
|
+
```
|
65
|
+
|
66
|
+
See the [Configuration](#configuration) section for more information on the available configurations.
|
67
|
+
|
68
|
+
### Pagination
|
69
|
+
|
70
|
+
When listing resources (e.g. transactions), **Increase** limits the number of results per page to 100. Luckily, the
|
71
|
+
client will automatically paginate through all the results for you!
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
Increase::Transactions.list(limit: :all) do |transactions|
|
75
|
+
# This block will be called once for each page of results
|
76
|
+
puts "I got #{transactions.count} transactions!"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Or, if you'd like a gargantuan array of all the transactions
|
80
|
+
Increase::Transactions.list(limit: :all)
|
81
|
+
Increase::Transactions.list(limit: 2_000)
|
82
|
+
```
|
83
|
+
|
84
|
+
Watch out for the rate limit!
|
85
|
+
|
86
|
+
### Error Handling
|
87
|
+
|
88
|
+
Whenever you make an oopsies, the client will raise an error! Errors originating from the API will be a subclass
|
89
|
+
of `Increase::ApiError`.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
|
93
|
+
begin
|
94
|
+
Increase::Transactions.retrieve('transaction_1234abcd')
|
95
|
+
rescue Increase::ApiError => e
|
96
|
+
puts e.message # "[404: object_not_found_error] Could not find the specified object. No resource of type ..."
|
97
|
+
puts e.title # "Could not find the specified object."
|
98
|
+
puts e.detail # "No resource of type transaction was found with ID transaction_1234abcd."
|
99
|
+
puts e.status # 404
|
100
|
+
|
101
|
+
puts e.response # This contains the full response from the API, including headers! (its a Faraday::Env object)
|
102
|
+
|
103
|
+
puts e.class # Increase::ObjectNotFoundError (it's a subclass of Increase::ApiError!)
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
To disable this behavior, set `Increase.raise_api_errors = false`. Errors will then be returned as a normal response.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
Increase.raise_api_errors = false # Default: true
|
111
|
+
|
112
|
+
Increase::Transactions.retrieve('transaction_1234abcd')
|
113
|
+
# => {"status"=>404, "type"=>"object_not_found_error", ... }
|
114
|
+
```
|
115
|
+
|
116
|
+
### Configuration
|
117
|
+
|
118
|
+
| Name | Description | Default |
|
119
|
+
|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|
|
120
|
+
| **api_key** | Your Increase API Key. Grab it from https://dashboard.increase.com/developers/api_keys | `nil` |
|
121
|
+
| **base_url** | The base URL for Increase's API. You can use `:production` (https://api.increase.com), `:sandbox` (https://sandbox.increase.com), or set an actual URL | `"https://api.increase.com"` |
|
122
|
+
| **raise_api_errors** | Whether to raise an error when the API returns a non-2XX status. Learn more about Increase's errors [here](https://increase.com/documentation/api#errors). See error classes [here](https://github.com/garyhtou/increase-ruby/blob/main/lib/increase/errors.rb) | `true` |
|
123
|
+
|
124
|
+
There are multiple syntaxes for configuring the client. Choose your favorite!
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
# Set the configurations directly
|
128
|
+
Increase.api_key = 'terabytes_of_cash' # Default: nil (you'll need one tho!)
|
129
|
+
Increase.base_url = :production # Default: :production
|
130
|
+
Increase.raise_api_errors = true # Default: true
|
131
|
+
|
132
|
+
# Or, you can use a block
|
133
|
+
Increase.configure do |config|
|
134
|
+
config.api_key = 'digital_dough'
|
135
|
+
config.base_url = :sandbox
|
136
|
+
config.raise_api_errors = false
|
137
|
+
end
|
138
|
+
|
139
|
+
# Or, you can pass in a hash
|
140
|
+
Increase.configure(api_key: 'just_my_two_cents')
|
141
|
+
```
|
142
|
+
|
143
|
+
### Idempotency
|
144
|
+
|
145
|
+
**Increase** supports [idempotent requests](https://increase.com/documentation/api#idempotency) to allow for safely
|
146
|
+
retrying requests without accidentally performing the same operation twice.
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
card = Increase::Cards.create(
|
150
|
+
{
|
151
|
+
# Card parameters
|
152
|
+
account_id: 'account_1234abcd',
|
153
|
+
description: 'My Chipotle card'
|
154
|
+
},
|
155
|
+
{
|
156
|
+
# Request headers
|
157
|
+
'Idempotency-Key': 'use a V4 UUID here'
|
158
|
+
}
|
159
|
+
)
|
160
|
+
# => {"id"=>"card_1234abcd", "type"=>"card", ... }
|
161
|
+
|
162
|
+
idempotent_replayed = card.response.headers['Idempotent-Replayed']
|
163
|
+
# => "false"
|
164
|
+
```
|
165
|
+
|
166
|
+
Reusing the key in subsequent requests will return the same response code and body as the original request along with an
|
167
|
+
additional HTTP header (Idempotent-Replayed: true). This applies to both success and error responses. In situations
|
168
|
+
where your request results in a validation error, you'll need to update your request and retry with a new idempotency
|
169
|
+
key.
|
22
170
|
|
23
171
|
## Development
|
24
172
|
|
25
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
173
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
|
174
|
+
also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
175
|
|
27
|
-
|
176
|
+
You can also run `INCREASE_API_KEY=my_key_here INCREASE_BASE_URL=https://sandbox.increase.com bin/console` to run the
|
177
|
+
console with your Increase sandbox API key pre-filled.
|
178
|
+
|
179
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
180
|
+
|
181
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will
|
182
|
+
create a git tag for the version, push git commits and the created tag, and push the `.gem` file
|
183
|
+
to [rubygems.org](https://rubygems.org).
|
184
|
+
|
185
|
+
Alternatively, use [`gem-release`](https://github.com/svenfuchs/gem-release) and
|
186
|
+
run `gem bump --version patch|minor|major`. Then release the gem by running `bundle exec rake release`.
|
28
187
|
|
29
188
|
## Contributing
|
30
189
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
190
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/garyhtou/increase.
|
32
191
|
|
33
192
|
## License
|
34
193
|
|
35
|
-
The gem is available as open source under the terms of
|
194
|
+
The gem is available as open source under the terms of
|
195
|
+
the [MIT License](https://github.com/garyhtou/increase-ruby/blob/main/LICENSE.txt).
|
196
|
+
|
197
|
+
---
|
198
|
+
|
199
|
+
Please note that this is not an official library written by **Increase**. Its written and maintained
|
200
|
+
by [Gary Tou](https://garytou.com/) who just uses Increase at work!
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "increase/configuration"
|
4
|
+
|
5
|
+
require "faraday"
|
6
|
+
require "faraday/follow_redirects"
|
7
|
+
|
8
|
+
# Custom Faraday Middleware to handle raising errors
|
9
|
+
require "faraday/raise_increase_api_error"
|
10
|
+
|
11
|
+
module Increase
|
12
|
+
class Client
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
attr_accessor :configuration
|
16
|
+
def_delegators :configuration, :configure
|
17
|
+
|
18
|
+
def_delegators :configuration, :base_url, :base_url=
|
19
|
+
def_delegators :configuration, :api_key, :api_key=
|
20
|
+
def_delegators :configuration, :raise_api_errors, :raise_api_errors=
|
21
|
+
|
22
|
+
def initialize(config = nil)
|
23
|
+
@configuration = config.is_a?(Configuration) ? config : Configuration.new(config)
|
24
|
+
end
|
25
|
+
|
26
|
+
def connection
|
27
|
+
Faraday.new(
|
28
|
+
url: @configuration.base_url,
|
29
|
+
headers: {
|
30
|
+
Authorization: "Bearer #{@configuration.api_key}",
|
31
|
+
"User-Agent": "Increase Ruby Gem v#{Increase::VERSION} (https://github.com/garyhtou/increase-ruby)"
|
32
|
+
}
|
33
|
+
) do |f|
|
34
|
+
f.request :json
|
35
|
+
|
36
|
+
if @configuration.raise_api_errors
|
37
|
+
# This custom middleware for raising Increase API errors must be
|
38
|
+
# located before the JSON response middleware.
|
39
|
+
f.use FaradayMiddleware::RaiseIncreaseApiError
|
40
|
+
end
|
41
|
+
|
42
|
+
f.response :json
|
43
|
+
f.response :follow_redirects
|
44
|
+
f.adapter Faraday.default_adapter
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Increase
|
4
|
+
class Configuration
|
5
|
+
attr_reader :base_url
|
6
|
+
attr_accessor :api_key
|
7
|
+
attr_accessor :raise_api_errors
|
8
|
+
# TODO: support Faraday config
|
9
|
+
|
10
|
+
def initialize(config = nil)
|
11
|
+
reset
|
12
|
+
configure(config) if config
|
13
|
+
end
|
14
|
+
|
15
|
+
def reset
|
16
|
+
@base_url = ENV["INCREASE_BASE_URL"] || Increase::PRODUCTION_URL
|
17
|
+
@api_key = ENV["INCREASE_API_KEY"]
|
18
|
+
@raise_api_errors = true
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure(config = nil)
|
22
|
+
if config.is_a?(Hash)
|
23
|
+
config.each do |key, value|
|
24
|
+
unless respond_to?("#{key}=")
|
25
|
+
raise Error, "Invalid configuration key: #{key}"
|
26
|
+
end
|
27
|
+
public_send("#{key}=", value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if block_given?
|
32
|
+
yield self
|
33
|
+
end
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def base_url=(url)
|
39
|
+
url = PRODUCTION_URL if url == :production
|
40
|
+
url = SANDBOX_URL if [:sandbox, :development].include?(url)
|
41
|
+
|
42
|
+
# Validate url
|
43
|
+
unless url&.match?(URI::DEFAULT_PARSER.make_regexp(%w[http https]))
|
44
|
+
raise ArgumentError, "Invalid url: #{url}"
|
45
|
+
end
|
46
|
+
|
47
|
+
@base_url = url
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Increase
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class ApiError < Error
|
7
|
+
attr_reader :response
|
8
|
+
|
9
|
+
attr_reader :detail
|
10
|
+
attr_reader :status
|
11
|
+
attr_reader :title
|
12
|
+
attr_reader :type
|
13
|
+
|
14
|
+
def initialize(message, response: nil, detail: nil, status: nil, title: nil, type: nil)
|
15
|
+
@response = response
|
16
|
+
|
17
|
+
@detail = detail || response.body["detail"]
|
18
|
+
@status = status || response.body["status"]
|
19
|
+
@title = title || response.body["title"]
|
20
|
+
@type = type || response.body["type"]
|
21
|
+
|
22
|
+
super(message)
|
23
|
+
end
|
24
|
+
|
25
|
+
class << self
|
26
|
+
def from_response(response)
|
27
|
+
type = response.body["type"]
|
28
|
+
klass = ERROR_TYPES[type]
|
29
|
+
|
30
|
+
# Fallback in case of really bad 5xx error
|
31
|
+
klass ||= InternalServerError if (500..599).cover?(response.status)
|
32
|
+
|
33
|
+
# Handle case of an unknown error
|
34
|
+
klass ||= ApiError
|
35
|
+
|
36
|
+
code = [response.body["status"] || response.status, type].compact.join(": ") || "Error"
|
37
|
+
message = [response.body["title"], response.body["detail"]].compact.join(" ") || "Increase API Error"
|
38
|
+
|
39
|
+
klass.new("[#{code}] #{message}", response: response)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class ApiMethodNotFoundError < ApiError; end
|
45
|
+
|
46
|
+
class EnvironmentMismatchError < ApiError; end
|
47
|
+
|
48
|
+
class IdempotencyConflictError < ApiError; end
|
49
|
+
|
50
|
+
class IdempotencyUnprocessableError < ApiError; end
|
51
|
+
|
52
|
+
class InsufficientPermissionsError < ApiError; end
|
53
|
+
|
54
|
+
class InternalServerError < ApiError; end
|
55
|
+
|
56
|
+
class InvalidApiKeyError < ApiError; end
|
57
|
+
|
58
|
+
class InvalidOperationError < ApiError; end
|
59
|
+
|
60
|
+
class InvalidParametersError < ApiError; end
|
61
|
+
|
62
|
+
class MalformedRequestError < ApiError; end
|
63
|
+
|
64
|
+
class ObjectNotFoundError < ApiError; end
|
65
|
+
|
66
|
+
class PrivateFeatureError < ApiError; end
|
67
|
+
|
68
|
+
class RateLimitedError < ApiError; end
|
69
|
+
|
70
|
+
ERROR_TYPES = {
|
71
|
+
"api_method_not_found_error" => ApiMethodNotFoundError,
|
72
|
+
"environment_mismatch_error" => EnvironmentMismatchError,
|
73
|
+
"idempotency_conflict_error" => IdempotencyConflictError,
|
74
|
+
"idempotency_unprocessable_error" => IdempotencyUnprocessableError,
|
75
|
+
"insufficient_permissions_error" => InsufficientPermissionsError,
|
76
|
+
"internal_server_error" => InternalServerError,
|
77
|
+
"invalid_api_key_error" => InvalidApiKeyError,
|
78
|
+
"invalid_operation_error" => InvalidOperationError,
|
79
|
+
"invalid_parameters_error" => InvalidParametersError,
|
80
|
+
"malformed_request_error" => MalformedRequestError,
|
81
|
+
"object_not_found_error" => ObjectNotFoundError,
|
82
|
+
"private_feature_error" => PrivateFeatureError,
|
83
|
+
"rate_limited_error" => RateLimitedError
|
84
|
+
}
|
85
|
+
|
86
|
+
# WebhookSignatureVerificationError is raised when a received webhook's
|
87
|
+
# signature is invalid.
|
88
|
+
class WebhookSignatureVerificationError < Error
|
89
|
+
attr_reader :signature_header
|
90
|
+
attr_reader :payload
|
91
|
+
|
92
|
+
def initialize(message = "Increase webhook signature verification failed", signature_header: nil, payload: nil)
|
93
|
+
@signature_header = signature_header
|
94
|
+
@payload = payload
|
95
|
+
|
96
|
+
super(message)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
require "increase/response_hash"
|
2
|
+
|
3
|
+
module Increase
|
4
|
+
class Resource
|
5
|
+
def initialize(client: nil)
|
6
|
+
if instance_of?(Resource)
|
7
|
+
raise NotImplementedError, "Resource is an abstract class. You should perform actions on its subclasses (Accounts, Transactions, Card, etc.)"
|
8
|
+
end
|
9
|
+
@client = client || Increase.default_client
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.with_config(config)
|
13
|
+
if config.is_a?(Client)
|
14
|
+
new(client: config)
|
15
|
+
else
|
16
|
+
new(client: Client.new(config))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.resource_url
|
21
|
+
"/#{resource_name.downcase.tr(" ", "_")}"
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.resource_name
|
25
|
+
if self == Resource
|
26
|
+
raise NotImplementedError, "Resource is an abstract class. You should perform actions on its subclasses (Accounts, Transactions, Card, etc.)"
|
27
|
+
end
|
28
|
+
|
29
|
+
name.split("::").last.gsub(/[A-Z]/, ' \0').strip
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.endpoint(name, http_method, to: :same_as_name, with: nil)
|
33
|
+
to = nil if to == :root
|
34
|
+
to = name.to_s if to == :same_as_name
|
35
|
+
to = [to].flatten.compact
|
36
|
+
with = [with].flatten.compact
|
37
|
+
|
38
|
+
raise Error, "Invalid `to`. Max of 2 elements allowed" if to.size > 2
|
39
|
+
raise Error, "Only one `to` allowed when not `with` an `id`" if to.size > 1 && !with.include?(:id)
|
40
|
+
|
41
|
+
request_method = :request
|
42
|
+
request_method = :paginated_request if with.include?(:pagination)
|
43
|
+
|
44
|
+
method =
|
45
|
+
if with.include?(:id)
|
46
|
+
# Method signature with a required `id` param
|
47
|
+
->(id, params = nil, headers = nil, &block) do
|
48
|
+
url = self.class.resource_url
|
49
|
+
url +=
|
50
|
+
if to.size == 2
|
51
|
+
"/#{to[0]}/#{id}/#{to[1]}"
|
52
|
+
elsif to.size == 1
|
53
|
+
# Default to id first
|
54
|
+
"/#{id}/#{to[0]}"
|
55
|
+
else
|
56
|
+
"/#{id}"
|
57
|
+
end
|
58
|
+
|
59
|
+
send(request_method, http_method, url, params, headers, &block)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
# Method signature without a required `id` param
|
63
|
+
->(params = nil, headers = nil, &block) do
|
64
|
+
url = self.class.resource_url
|
65
|
+
url += "/#{to[0]}" if to.size == 1
|
66
|
+
|
67
|
+
send(request_method, http_method, url, params, headers, &block)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Define instance method
|
72
|
+
define_method(name, &method)
|
73
|
+
|
74
|
+
# Define class method (uses default config by calling `new`)
|
75
|
+
define_singleton_method(name) do |*args, &block|
|
76
|
+
new.send(name, *args, &block)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private_class_method :endpoint
|
81
|
+
|
82
|
+
class << self
|
83
|
+
private
|
84
|
+
|
85
|
+
# These methods here are shortcuts for the `endpoint` method. They define
|
86
|
+
# commonly used endpoints. For example, nearly all resources have a `list`
|
87
|
+
# endpoint which is a `GET` request to the resource's root URL.
|
88
|
+
|
89
|
+
def create
|
90
|
+
endpoint :create, :post, to: :root
|
91
|
+
end
|
92
|
+
|
93
|
+
def list
|
94
|
+
endpoint :list, :get, to: :root, with: :pagination
|
95
|
+
end
|
96
|
+
|
97
|
+
def update
|
98
|
+
endpoint :update, :patch, to: :root, with: :id
|
99
|
+
end
|
100
|
+
|
101
|
+
def retrieve
|
102
|
+
endpoint :retrieve, :get, to: :root, with: :id
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# def self.endpoint_action(method, http_method)
|
107
|
+
# define_singleton_method(method) do |*args, &block|
|
108
|
+
# new.send(:action, method, http_method, *args, &block)
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
# define_method(method) do |*args, &block|
|
112
|
+
# new.send(:action, method, http_method, *args, &block)
|
113
|
+
# end
|
114
|
+
# end
|
115
|
+
#
|
116
|
+
# private_class_method :endpoint_action
|
117
|
+
#
|
118
|
+
# private
|
119
|
+
#
|
120
|
+
# def create(params = nil, headers = nil)
|
121
|
+
# request(:post, self.class.resource_url, params, headers)
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# def list(params = nil, headers = nil, &block)
|
125
|
+
# results = []
|
126
|
+
# count = 0
|
127
|
+
# limit = params&.[](:limit) || params&.[]("limit")
|
128
|
+
# if limit == :all || limit&.>(100)
|
129
|
+
# params&.delete(:limit)
|
130
|
+
# params&.delete("limit")
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# loop do
|
134
|
+
# res = request(:get, self.class.resource_url, params, headers)
|
135
|
+
# data = res["data"]
|
136
|
+
# count += data.size
|
137
|
+
# if ![nil, :all].include?(limit) && count >= limit
|
138
|
+
# data = data[0..(limit - (count - data.size) - 1)]
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# if block
|
142
|
+
# block.call(data)
|
143
|
+
# else
|
144
|
+
# results += data
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# if limit.nil? || (limit != :all && count >= limit) || res["next_cursor"].nil?
|
148
|
+
# if block
|
149
|
+
# break
|
150
|
+
# else
|
151
|
+
# return results
|
152
|
+
# end
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# params = (params || {}).merge({ cursor: res["next_cursor"] })
|
156
|
+
# end
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# def update(id, params = nil, headers = nil)
|
160
|
+
# raise Error, "id must be a string" unless id.is_a?(String)
|
161
|
+
# path = "#{self.class.resource_url}/#{id}"
|
162
|
+
# request(:patch, path, params, headers)
|
163
|
+
# end
|
164
|
+
#
|
165
|
+
# def retrieve(id, params = nil, headers = nil)
|
166
|
+
# raise Error, "id must be a string" unless id.is_a?(String)
|
167
|
+
# path = "#{self.class.resource_url}/#{id}"
|
168
|
+
# request(:get, path, params, headers)
|
169
|
+
# end
|
170
|
+
#
|
171
|
+
# # Such as for "/accounts/{account_id}/close"
|
172
|
+
# # "close" is the action.
|
173
|
+
# def action(action, http_method, id, params = nil, headers = nil)
|
174
|
+
# raise Error, "id must be a string" unless id.is_a?(String)
|
175
|
+
# path = "#{self.class.resource_url}/#{id}/#{action}"
|
176
|
+
# request(http_method, path, params, headers)
|
177
|
+
# end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def request(method, path, params = nil, headers = nil, &block)
|
182
|
+
if block
|
183
|
+
# Assume the caller wants to automatically paginate
|
184
|
+
return paginated_request(method, path, params, headers, &block)
|
185
|
+
end
|
186
|
+
|
187
|
+
if method == :post
|
188
|
+
headers = {"Content-Type" => "application/json"}.merge!(headers || {})
|
189
|
+
end
|
190
|
+
|
191
|
+
response = @client.connection.send(method, path, params, headers)
|
192
|
+
ResponseHash.new(response.body, response: response)
|
193
|
+
end
|
194
|
+
|
195
|
+
def paginated_request(method, path, params = nil, headers = nil, &block)
|
196
|
+
results = []
|
197
|
+
count = 0
|
198
|
+
limit = params&.[](:limit) || params&.[]("limit")
|
199
|
+
if limit == :all || limit&.>(100)
|
200
|
+
params&.delete(:limit)
|
201
|
+
params&.delete("limit")
|
202
|
+
end
|
203
|
+
|
204
|
+
loop do
|
205
|
+
res = request(method, path, params, headers)
|
206
|
+
data = res["data"]
|
207
|
+
|
208
|
+
# Handle case where endpoint doesn't actually support pagination.
|
209
|
+
# For example, someone passes a block to `Account.create`
|
210
|
+
if data.nil?
|
211
|
+
# In this case, we'll both yield and return the response
|
212
|
+
yield res if block
|
213
|
+
return res
|
214
|
+
end
|
215
|
+
|
216
|
+
count += data.size
|
217
|
+
if ![nil, :all].include?(limit) && count >= limit
|
218
|
+
data = data[0..(limit - (count - data.size) - 1)]
|
219
|
+
end
|
220
|
+
|
221
|
+
if block
|
222
|
+
block.call(data)
|
223
|
+
else
|
224
|
+
results += data
|
225
|
+
end
|
226
|
+
|
227
|
+
if limit.nil? || (limit != :all && count >= limit) || res["next_cursor"].nil?
|
228
|
+
if block
|
229
|
+
break
|
230
|
+
else
|
231
|
+
return results
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
params = (params || {}).merge({cursor: res["next_cursor"]})
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Increase
|
2
|
+
class ResponseHash < Hash
|
3
|
+
attr_reader :response
|
4
|
+
|
5
|
+
def initialize(hash, response: nil)
|
6
|
+
@response = response
|
7
|
+
merge!(hash)
|
8
|
+
end
|
9
|
+
|
10
|
+
# https://increase.com/documentation/api#idempotency
|
11
|
+
def idempotent_replayed
|
12
|
+
response.headers["Idempotent-Replayed"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Increase
|
2
|
+
module Util
|
3
|
+
# Constant time string comparison to prevent timing attacks
|
4
|
+
# Code borrowed from `stripe-ruby`, which was borrowed from ActiveSupport
|
5
|
+
def self.secure_compare(a, b)
|
6
|
+
return false unless a.bytesize == b.bytesize
|
7
|
+
|
8
|
+
l = a.unpack "C#{a.bytesize}"
|
9
|
+
|
10
|
+
res = 0
|
11
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
12
|
+
res.zero?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/increase/version.rb
CHANGED
@@ -0,0 +1,56 @@
|
|
1
|
+
require "increase/util"
|
2
|
+
require "increase/errors"
|
3
|
+
|
4
|
+
module Increase
|
5
|
+
# Keeping this module singular in case Increase adds a `webhooks` resource
|
6
|
+
module Webhook
|
7
|
+
module Signature
|
8
|
+
DEFAULT_TIME_TOLERANCE = 300 # 300 seconds (5 minutes)
|
9
|
+
DEFAULT_SCHEME = "v1"
|
10
|
+
|
11
|
+
def self.verify?(payload:, signature_header:, secret:, scheme: DEFAULT_SCHEME, time_tolerance: DEFAULT_TIME_TOLERANCE)
|
12
|
+
# Helper for raising errors with additional metadata
|
13
|
+
sig_error = ->(msg) do
|
14
|
+
WebhookSignatureVerificationError.new(msg, signature_header: signature_header, payload: payload)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Parse header
|
18
|
+
sig_values = signature_header.split(",").map { |pair| pair.split("=") }.to_h
|
19
|
+
|
20
|
+
# Extract values
|
21
|
+
t = sig_values["t"] # Should be a string (ISO-8601 timestamp)
|
22
|
+
sig = sig_values[scheme]
|
23
|
+
raise sig_error.call("No timestamp found in signature header") if t.nil?
|
24
|
+
raise sig_error.call("No signature found with scheme #{scheme} in signature header") if sig.nil?
|
25
|
+
|
26
|
+
# Check signature
|
27
|
+
expected_sig = compute_signature(timestamp: t, payload: payload, secret: secret)
|
28
|
+
matches = Util.secure_compare(expected_sig, sig)
|
29
|
+
raise sig_error.call("Signature mismatch") unless matches
|
30
|
+
|
31
|
+
# Check timestamp tolerance to prevent timing attacks
|
32
|
+
if time_tolerance > 0
|
33
|
+
begin
|
34
|
+
timestamp = DateTime.parse(t)
|
35
|
+
now = DateTime.now
|
36
|
+
diff = (now - timestamp) * 24 * 60 * 60 # in seconds
|
37
|
+
|
38
|
+
# Don't allow timestamps in the future
|
39
|
+
if diff > time_tolerance || diff < 0
|
40
|
+
raise sig_error.call("Timestamp outside of the tolerance zone")
|
41
|
+
end
|
42
|
+
rescue Date::Error
|
43
|
+
raise sig_error.call("Invalid timestamp in signature header: #{t}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.compute_signature(timestamp:, payload:, secret:)
|
51
|
+
signed_payload = timestamp.to_s + "." + payload.to_s
|
52
|
+
OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/increase.rb
CHANGED
@@ -1,8 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "increase/version"
|
4
|
+
require "increase/client"
|
5
|
+
require "increase/configuration"
|
6
|
+
require "increase/errors"
|
7
|
+
require "increase/resources"
|
8
|
+
require "increase/webhook/signature"
|
4
9
|
|
5
10
|
module Increase
|
6
|
-
|
7
|
-
|
11
|
+
PRODUCTION_URL = "https://api.increase.com"
|
12
|
+
SANDBOX_URL = "https://sandbox.increase.com"
|
13
|
+
|
14
|
+
@default_client = Client.new
|
15
|
+
|
16
|
+
class << self
|
17
|
+
extend Forwardable
|
18
|
+
|
19
|
+
attr_accessor :default_client
|
20
|
+
def_delegators :default_client, :configure
|
21
|
+
|
22
|
+
def_delegators :default_client, :base_url, :base_url=
|
23
|
+
def_delegators :default_client, :api_key, :api_key=
|
24
|
+
def_delegators :default_client, :raise_api_errors, :raise_api_errors=
|
25
|
+
end
|
8
26
|
end
|
metadata
CHANGED
@@ -1,15 +1,113 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: increase
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gary Tou
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
12
|
-
dependencies:
|
11
|
+
date: 2023-03-20 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.7'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday-follow_redirects
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.3.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '13.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '13.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: standard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.13'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.13'
|
13
111
|
description: Ruby API client for Increase, a platform for Bare-Metal Banking APIs
|
14
112
|
email:
|
15
113
|
- gary@garytou.com
|
@@ -23,8 +121,25 @@ files:
|
|
23
121
|
- LICENSE.txt
|
24
122
|
- README.md
|
25
123
|
- Rakefile
|
124
|
+
- lib/faraday/raise_increase_api_error.rb
|
26
125
|
- lib/increase.rb
|
126
|
+
- lib/increase/client.rb
|
127
|
+
- lib/increase/configuration.rb
|
128
|
+
- lib/increase/errors.rb
|
129
|
+
- lib/increase/resource.rb
|
130
|
+
- lib/increase/resources.rb
|
131
|
+
- lib/increase/resources/account_numbers.rb
|
132
|
+
- lib/increase/resources/account_transfers.rb
|
133
|
+
- lib/increase/resources/accounts.rb
|
134
|
+
- lib/increase/resources/ach_transfers.rb
|
135
|
+
- lib/increase/resources/cards.rb
|
136
|
+
- lib/increase/resources/events.rb
|
137
|
+
- lib/increase/resources/pending_transactions.rb
|
138
|
+
- lib/increase/resources/transactions.rb
|
139
|
+
- lib/increase/response_hash.rb
|
140
|
+
- lib/increase/util.rb
|
27
141
|
- lib/increase/version.rb
|
142
|
+
- lib/increase/webhook/signature.rb
|
28
143
|
- sig/increase.rbs
|
29
144
|
homepage: https://github.com/garyhtou/increase-ruby
|
30
145
|
licenses:
|
@@ -41,7 +156,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
156
|
requirements:
|
42
157
|
- - ">="
|
43
158
|
- !ruby/object:Gem::Version
|
44
|
-
version: 2.
|
159
|
+
version: 2.7.4
|
45
160
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
161
|
requirements:
|
47
162
|
- - ">="
|