medrare-gocardless 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ lib/*seed*.rb
2
+ lib/example.rb
3
+ lib/*test*.rb
4
+ doc/
5
+ Gemfile.lock
6
+ gocardless*.gem
7
+ .yardoc
8
+ .yardopts
9
+ gocardless-*.gem
10
+ gocardless-ruby.zip
11
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,16 @@
1
+ script: "rake spec"
2
+ rvm:
3
+ - 1.9.3
4
+ - 1.9.2
5
+ - 1.8.7
6
+ - jruby-18mode
7
+ - jruby-19mode
8
+ - rbx-18mode
9
+ - ruby-head
10
+ - jruby-head
11
+ - ree
12
+ notifications:
13
+ email:
14
+ recipients:
15
+ - developers@gocardless.com
16
+
data/CHANGELOG.md ADDED
@@ -0,0 +1,68 @@
1
+ ## 1.2.1 - July 11, 2012
2
+
3
+ - Fix bug which caused Client#merchant to fail after #fetch_access_token was
4
+ called during the merchant authorization flow (this only concerns partners).
5
+
6
+ ## 1.2.0 - June 19, 2012
7
+
8
+ - Add some extra attributes to resources (e.g. status, merchant's balance, etc)
9
+ - Add a response_params_valid? method to check that resource response data is
10
+ valid (including signature)
11
+
12
+ ## 1.1.1 - June 07, 2012
13
+
14
+ - Fix handling of cancel_uri
15
+
16
+
17
+ ## 1.1.0 - May 25, 2012
18
+
19
+ - Accept merchant_id as a client constructor param
20
+
21
+
22
+ ## 1.0.1 - May 14, 2012
23
+
24
+ - Update oauth2 dependency version, fixes installation issue
25
+
26
+
27
+ ## 1.0.0 - April 24, 2012
28
+
29
+ - Add plan_id to selected resources
30
+ - Remove deprecated resource attributes
31
+ - Fix sorting issue in Utils.normalize_params
32
+ - Add rake console task
33
+ - Add rake version:bump tasks
34
+ - Fix user agent formatting
35
+ - Relax multi_json dependency
36
+
37
+
38
+ ## 0.2.0 - April 3, 2012
39
+
40
+ - Add `cancel!` method to `Subscription`
41
+ - Depend on multi_json rather than json
42
+ - Include the API version in the user agent header
43
+
44
+
45
+ ## 0.1.3 - February 22, 2012
46
+
47
+ - Fix parameter encoding in `Client#new_merchant_url` (related to Faraday issue
48
+ #115)
49
+ - Allow changing environment / base_url after client init
50
+
51
+
52
+ ## 0.1.2 - February 2, 2012
53
+
54
+ - Add `webhook_valid?` method to `Client`
55
+ - Make `confirm_resource` play nicely with `HashWithIndifferentAccess`
56
+ - More RFC compliant parameter encoding
57
+
58
+
59
+ ## 0.1.1 - January 12, 2012
60
+
61
+ - Add `name` to `Bill`
62
+ - Add `state` support to `new_{subscription,pre_authorization,bill}_url`
63
+
64
+
65
+ ## 0.1.0 - November 23, 2011
66
+
67
+ - Initial release
68
+
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem "guard", "~> 0.8.8"
7
+ if RUBY_PLATFORM.downcase.include?("darwin")
8
+ gem "guard-rspec", "~> 0.5.4"
9
+ gem "rb-fsevent", "~> 0.9"
10
+ gem "growl", "~> 1.0.3"
11
+ end
12
+ end
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec', :version => 2, :cli => '--color --format doc' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/gocardless/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('lib/gocardless.rb') { "spec/gocardless_spec.rb" }
5
+ watch('spec/spec_helper.rb') { "spec" }
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 GoCardless
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,17 @@
1
+ ![GoCardless](https://gocardless.com/resources/logo.png)
2
+
3
+ ## GoCardless Ruby Client Library
4
+
5
+ The GoCardless Ruby client provides a simple Ruby interface to the GoCardless
6
+ API.
7
+
8
+ If you want to use the library as an individual merchant, refer to the
9
+ [merchant guide](https://gocardless.com/docs/ruby/merchant_client_guide). If
10
+ you want to support multiple merchant accounts, see the
11
+ [partner guide](https://gocardless.com/docs/ruby/partner_client_guide).
12
+
13
+ The full API reference is available at on
14
+ [rubydoc.info](http://rubydoc.info/github/gocardless/gocardless-ruby/master/frames).
15
+
16
+ [![Build Status](https://secure.travis-ci.org/gocardless/gocardless-ruby.png?branch=master)](http://travis-ci.org/gocardless/gocardless-ruby)
17
+
data/Rakefile ADDED
@@ -0,0 +1,82 @@
1
+ require 'yard'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Generate YARD documentation"
5
+ YARD::Rake::YardocTask.new do |t|
6
+ files = ['lib/**/*.rb', '-', 'CHANGELOG.md', 'LICENSE']
7
+ t.files = files.reject { |f| f =~ /seed|example/ }
8
+ end
9
+
10
+ desc "Run an IRB session with gocardless pre-loaded"
11
+ task :console do
12
+ exec "irb -I lib -r gocardless"
13
+ end
14
+
15
+ desc "Run the specs"
16
+ RSpec::Core::RakeTask.new(:spec) do |t|
17
+ t.rspec_opts = %w[--color]
18
+ end
19
+
20
+
21
+ def generate_changelog(last_version, new_version)
22
+ commits = `git log v#{last_version}.. --oneline`.split("\n")
23
+ msgs = commits.map { |commit| commit.sub(/^[a-f0-9]+/, '-') }
24
+ date = Time.now.strftime("%B %d, %Y")
25
+ "## #{new_version} - #{date}\n\n#{msgs.join("\n")}\n\n\n"
26
+ end
27
+
28
+ def update_changelog(last_version, new_version)
29
+ contents = File.read('CHANGELOG.md')
30
+ if contents =~ /## #{new_version}/
31
+ puts "CHANGELOG already contains v#{new_version}, skipping"
32
+ return false
33
+ end
34
+ changelog = generate_changelog(last_version, new_version)
35
+ File.open('CHANGELOG.md', 'w') { |f| f.write(changelog + contents) }
36
+ end
37
+
38
+ def update_version_file(new_version)
39
+ path = "lib/#{Dir.glob('*.gemspec').first.split('.').first}/version.rb"
40
+ contents = File.read(path)
41
+ contents.sub!(/VERSION\s+=\s+["'][\d\.]+["']/, "VERSION = '#{new_version}'")
42
+ File.open(path, 'w') { |f| f.write(contents) }
43
+ end
44
+
45
+ def bump_version(part)
46
+ last_version = `git tag -l | tail -1`.strip.sub(/^v/, '')
47
+ major, minor, patch = last_version.scan(/\d+/).map(&:to_i)
48
+
49
+ case part
50
+ when :major
51
+ major += 1
52
+ minor = patch = 0
53
+ when :minor
54
+ minor += 1
55
+ patch = 0
56
+ when :patch
57
+ patch += 1
58
+ end
59
+ new_version = "#{major}.#{minor}.#{patch}"
60
+
61
+ update_changelog(last_version, new_version)
62
+ puts "Updated CHANGELOG"
63
+
64
+ update_version_file(new_version)
65
+ puts "Updated version.rb"
66
+ end
67
+
68
+ desc "Update the version, auto-generating the changelog"
69
+ namespace :version do
70
+ namespace :bump do
71
+ task :major do
72
+ bump_version :major
73
+ end
74
+ task :minor do
75
+ bump_version :minor
76
+ end
77
+ task :patch do
78
+ bump_version :patch
79
+ end
80
+ end
81
+ end
82
+
@@ -0,0 +1,22 @@
1
+ require File.expand_path('../lib/gocardless/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.add_runtime_dependency 'oauth2', '~> 0.8.0'
5
+ gem.add_runtime_dependency 'multi_json', '~> 1.0'
6
+
7
+ gem.add_development_dependency 'rspec', '~> 2.6'
8
+ gem.add_development_dependency 'mocha', '~> 0.9.12'
9
+ gem.add_development_dependency 'yard', '~> 0.7.3'
10
+ gem.add_development_dependency 'activesupport', '~> 3.1'
11
+
12
+ gem.authors = ['Harry Marr', 'Tom Blomfield']
13
+ gem.description = %q{A Ruby wrapper for the GoCardless API}
14
+ gem.email = ['developers@gocardless.com']
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.homepage = 'https://github.com/gocardless/gocardless-ruby'
17
+ gem.name = 'medrare-gocardless'
18
+ gem.require_paths = ['lib']
19
+ gem.summary = %q{Ruby wrapper for the GoCardless API}
20
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
+ gem.version = GoCardless::VERSION.dup
22
+ end
data/lib/gocardless.rb ADDED
@@ -0,0 +1,33 @@
1
+ module GoCardless
2
+ require 'gocardless/version'
3
+ require 'gocardless/errors'
4
+ require 'gocardless/utils'
5
+ require 'gocardless/resource'
6
+ require 'gocardless/subscription'
7
+ require 'gocardless/pre_authorization'
8
+ require 'gocardless/user'
9
+ require 'gocardless/bill'
10
+ require 'gocardless/payment'
11
+ require 'gocardless/merchant'
12
+ require 'gocardless/client'
13
+
14
+ class << self
15
+ attr_accessor :environment
16
+ attr_reader :account_details, :client
17
+
18
+ def account_details=(details)
19
+ raise ClientError.new("You must provide a token") unless details[:token]
20
+ @account_details = details
21
+ @client = Client.new(details)
22
+ end
23
+
24
+ %w(new_subscription_url new_pre_authorization_url new_bill_url confirm_resource webhook_valid?).each do |name|
25
+ class_eval <<-EOM
26
+ def #{name}(*args)
27
+ raise ClientError.new('Need to set account_details first') unless @client
28
+ @client.send(:#{name}, *args)
29
+ end
30
+ EOM
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,48 @@
1
+ module GoCardless
2
+ class Bill < Resource
3
+ self.endpoint = '/bills/:id'
4
+
5
+ creatable
6
+
7
+ attr_accessor :amount,
8
+ :source_type,
9
+ :description,
10
+ :name,
11
+ :plan_id,
12
+ :status
13
+
14
+ # @attribute source_id
15
+ # @return [String] the ID of the bill's source (eg subscription, pre_authorization)
16
+ attr_accessor :source_id
17
+
18
+ reference_accessor :merchant_id, :user_id, :payment_id
19
+ date_accessor :created_at
20
+
21
+ def source
22
+ klass = GoCardless.const_get(Utils.camelize(source_type.to_s))
23
+ klass.find_with_client(client, @source_id)
24
+ end
25
+
26
+ def source=(obj)
27
+ klass = obj.class.to_s.split(':').last
28
+ if !%w{Subscription PreAuthorization}.include?(klass)
29
+ raise ArgumentError, ("Object must be an instance of Subscription or "
30
+ "PreAuthorization")
31
+ end
32
+ @source_id = obj.id
33
+ @source_type = Utils.underscore(klass)
34
+ end
35
+
36
+ def save
37
+ save_data({
38
+ :bill => {
39
+ :pre_authorization_id => self.source_id,
40
+ :amount => self.amount,
41
+ :name => self.name,
42
+ :description => self.description,
43
+ }
44
+ })
45
+ self
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,393 @@
1
+ require 'rubygems'
2
+ require 'multi_json'
3
+ require 'oauth2'
4
+ require 'openssl'
5
+ require 'uri'
6
+ require 'cgi'
7
+ require 'time'
8
+ require 'base64'
9
+
10
+ module GoCardless
11
+ class Client
12
+ BASE_URLS = {
13
+ :production => 'https://gocardless.com',
14
+ :sandbox => 'https://sandbox.gocardless.com',
15
+ }
16
+ API_PATH = '/api/v1'
17
+
18
+ class << self
19
+ def base_url=(url)
20
+ @base_url = url.sub(%r|/$|, '')
21
+ end
22
+
23
+ def base_url
24
+ @base_url || BASE_URLS[GoCardless.environment || :production]
25
+ end
26
+
27
+ def api_url
28
+ "#{base_url}#{API_PATH}"
29
+ end
30
+ end
31
+
32
+ def initialize(args = {})
33
+ Utils.symbolize_keys! args
34
+ @app_id = args[:app_id]
35
+ @app_secret = args[:app_secret]
36
+ raise ClientError.new("You must provide an app_id") unless @app_id
37
+ raise ClientError.new("You must provide an app_secret") unless @app_secret
38
+
39
+ @oauth_client = OAuth2::Client.new(@app_id, @app_secret,
40
+ :site => self.class.base_url,
41
+ :token_url => '/oauth/access_token')
42
+
43
+ self.access_token = args[:token] if args[:token]
44
+ @merchant_id = args[:merchant_id] if args[:merchant_id]
45
+ end
46
+
47
+ # Generate the OAuth authorize url
48
+ #
49
+ # @param [Hash] options parameters to be included in the url.
50
+ # +:redirect_uri+ is required.
51
+ # @return [String] the authorize url
52
+ def authorize_url(options)
53
+ raise ArgumentError, ':redirect_uri required' unless options[:redirect_uri]
54
+ params = {
55
+ :client_id => @app_id,
56
+ :response_type => 'code',
57
+ :scope => 'manage_merchant'
58
+ }
59
+ # Faraday doesn't flatten params in this case (Faraday issue #115)
60
+ options = Hash[Utils.flatten_params(options)]
61
+ @oauth_client.authorize_url(params.merge(options))
62
+ end
63
+ alias :new_merchant_url :authorize_url
64
+
65
+ # Exchange the authorization code for an access token
66
+ #
67
+ # @param [String] auth_code to exchange for the access_token
68
+ # @return [String] the access_token required to make API calls to resources
69
+ def fetch_access_token(auth_code, options)
70
+ raise ArgumentError, ':redirect_uri required' unless options[:redirect_uri]
71
+ # Exchange the auth code for an access token
72
+ @access_token = @oauth_client.auth_code.get_token(auth_code, options)
73
+
74
+ # Use the scope to figure out which merchant we're managing
75
+ scope = @access_token.params[:scope] || @access_token.params['scope']
76
+ set_merchant_id_from_scope(scope)
77
+
78
+ self.access_token
79
+ end
80
+
81
+ # @return [String] a serialized form of the access token with its scope
82
+ def access_token
83
+ if @access_token
84
+ scope = @access_token.params[:scope] || @access_token.params['scope']
85
+ "#{@access_token.token} #{scope}".strip
86
+ end
87
+ end
88
+
89
+ # Set the client's access token
90
+ #
91
+ # @param [String] token a string with format <code>"#{token} #{scope}"</code>
92
+ # (as returned by {#access_token})
93
+ def access_token=(token)
94
+ token, scope = token.sub(/^bearer\s+/i, '').split(' ', 2)
95
+ scope ||= ''
96
+
97
+ @access_token = OAuth2::AccessToken.new(@oauth_client, token)
98
+ @access_token.params['scope'] = scope
99
+
100
+ set_merchant_id_from_scope(scope) unless @merchant_id
101
+ end
102
+
103
+ # Issue an GET request to the API server
104
+ #
105
+ # @note this method is for internal use
106
+ # @param [String] path the path that will be added to the API prefix
107
+ # @param [Hash] params query string parameters
108
+ # @return [Hash] hash the parsed response data
109
+ def api_get(path, params = {})
110
+ request(:get, "#{API_PATH}#{path}", :params => params).parsed
111
+ end
112
+
113
+ # Issue a POST request to the API server
114
+ #
115
+ # @note this method is for internal use
116
+ # @param [String] path the path that will be added to the API prefix
117
+ # @param [Hash] data a hash of data that will be sent as the request body
118
+ # @return [Hash] hash the parsed response data
119
+ def api_post(path, data = {})
120
+ request(:post, "#{API_PATH}#{path}", :data => data).parsed
121
+ end
122
+
123
+ # Issue a PUT request to the API server
124
+ #
125
+ # @note this method is for internal use
126
+ # @param [String] path the path that will be added to the API prefix
127
+ # @param [Hash] data a hash of data that will be sent as the request body
128
+ # @return [Hash] hash the parsed response data
129
+ def api_put(path, data = {})
130
+ request(:put, "#{API_PATH}#{path}", :data => data).parsed
131
+ end
132
+
133
+ # @method merchant
134
+ # @return [Merchant] the merchant associated with the client's access token
135
+ def merchant
136
+ raise ClientError, 'Access token missing' unless @access_token
137
+ Merchant.new_with_client(self, api_get("/merchants/#{merchant_id}"))
138
+ end
139
+
140
+ # @method subscripton(id)
141
+ # @param [String] id of the subscription
142
+ # @return [Subscription] the subscription matching the id requested
143
+ def subscription(id)
144
+ Subscription.find_with_client(self, id)
145
+ end
146
+
147
+ # @method pre_authorization(id)
148
+ # @param [String] id of the pre_authorization
149
+ # @return [PreAuthorization] the pre_authorization matching the id requested
150
+ def pre_authorization(id)
151
+ PreAuthorization.find_with_client(self, id)
152
+ end
153
+
154
+ # @method user(id)
155
+ # @param [String] id of the user
156
+ # @return [User] the User matching the id requested
157
+ def user(id)
158
+ User.find_with_client(self, id)
159
+ end
160
+
161
+ # @method bill(id)
162
+ # @param [String] id of the bill
163
+ # @return [Bill] the Bill matching the id requested
164
+ def bill(id)
165
+ Bill.find_with_client(self, id)
166
+ end
167
+
168
+ # @method payment(id)
169
+ # @param [String] id of the payment
170
+ # @return [Payment] the payment matching the id requested
171
+ def payment(id)
172
+ Payment.find_with_client(self, id)
173
+ end
174
+
175
+ # Create a new bill under a given pre-authorization
176
+ # @see PreAuthorization#create_bill
177
+ #
178
+ # @param [Hash] attrs must include +:pre_authorization_id+ and +:amount+
179
+ # @return [Bill] the created bill object
180
+ def create_bill(attrs)
181
+ Bill.new_with_client(self, attrs).save
182
+ end
183
+
184
+ # Generate the URL for creating a new subscription. The parameters passed
185
+ # in define various attributes of the subscription. Redirecting a user to
186
+ # the resulting URL will show them a page where they can approve or reject
187
+ # the subscription described by the parameters. Note that this method
188
+ # automatically includes the nonce, timestamp and signature.
189
+ #
190
+ # @param [Hash] params the subscription parameters
191
+ # @return [String] the generated URL
192
+ def new_subscription_url(params)
193
+ new_limit_url(:subscription, params)
194
+ end
195
+
196
+ # Generate the URL for creating a new pre authorization. The parameters
197
+ # passed in define various attributes of the pre authorization. Redirecting
198
+ # a user to the resulting URL will show them a page where they can approve
199
+ # or reject the pre authorization described by the parameters. Note that
200
+ # this method automatically includes the nonce, timestamp and signature.
201
+ #
202
+ # @param [Hash] params the pre authorization parameters
203
+ # @return [String] the generated URL
204
+ def new_pre_authorization_url(params)
205
+ new_limit_url(:pre_authorization, params)
206
+ end
207
+
208
+ # Generate the URL for creating a new bill. The parameters passed in define
209
+ # various attributes of the bill. Redirecting a user to the resulting URL
210
+ # will show them a page where they can approve or reject the bill described
211
+ # by the parameters. Note that this method automatically includes the
212
+ # nonce, timestamp and signature.
213
+ #
214
+ # @param [Hash] params the bill parameters
215
+ # @return [String] the generated URL
216
+ def new_bill_url(params)
217
+ new_limit_url(:bill, params)
218
+ end
219
+
220
+ # Confirm a newly-created subscription, pre-authorzation or one-off
221
+ # payment. This method also checks that the resource response data includes
222
+ # a valid signature and will raise a {SignatureError} if the signature is
223
+ # invalid.
224
+ #
225
+ # @param [Hash] params the response parameters returned by the API server
226
+ # @return [Resource] the confirmed resource object
227
+ def confirm_resource(params)
228
+ params = prepare_params(params)
229
+
230
+ if signature_valid?(params)
231
+ data = {
232
+ :resource_id => params[:resource_id],
233
+ :resource_type => params[:resource_type],
234
+ }
235
+
236
+ credentials = Base64.encode64("#{@app_id}:#{@app_secret}")
237
+ credentials = credentials.gsub(/\s/, '')
238
+ headers = {
239
+ 'Authorization' => "Basic #{credentials}"
240
+ }
241
+ request(:post, "#{self.class.api_url}/confirm", :data => data,
242
+ :headers => headers)
243
+
244
+ # Initialize the correct class according to the resource's type
245
+ klass = GoCardless.const_get(Utils.camelize(params[:resource_type]))
246
+ klass.find_with_client(self, params[:resource_id])
247
+ else
248
+ raise SignatureError, 'An invalid signature was detected'
249
+ end
250
+ end
251
+
252
+
253
+ # Check that resource response data includes a valid signature.
254
+ #
255
+ # @param [Hash] params the response parameters returned by the API server
256
+ # @return [Boolean] true when valid, false otherwise
257
+ def response_params_valid?(params)
258
+ params = prepare_params(params)
259
+
260
+ signature_valid?(params)
261
+ end
262
+
263
+
264
+ # Validates the payload contents of a webhook request.
265
+ #
266
+ # @param [Hash] params the contents of payload of the webhook
267
+ # @return [Boolean] true when valid, false otherwise
268
+ def webhook_valid?(params)
269
+ signature_valid?(params)
270
+ end
271
+
272
+ private
273
+
274
+ # Return the merchant id, throwing a proper error if it's missing.
275
+ def merchant_id
276
+ raise ClientError, 'No merchant id set' unless @merchant_id
277
+ @merchant_id
278
+ end
279
+
280
+ # Pull the merchant id out of the access scope
281
+ def set_merchant_id_from_scope(scope)
282
+ perm = scope.split.select {|p| p.start_with?('manage_merchant:') }.first
283
+ @merchant_id = perm.split(':')[1] if perm
284
+ end
285
+
286
+ # Send a request to the GoCardless API servers
287
+ #
288
+ # @param [Symbol] method the HTTP method to use (e.g. +:get+, +:post+)
289
+ # @param [String] path the path fragment of the URL
290
+ # @option [Hash] opts query string parameters
291
+ def request(method, path, opts = {})
292
+ raise ClientError, 'Access token missing' unless @access_token
293
+
294
+ opts[:headers] = {} if opts[:headers].nil?
295
+ opts[:headers]['Accept'] = 'application/json'
296
+ opts[:headers]['Content-Type'] = 'application/json' unless method == :get
297
+ opts[:headers]['User-Agent'] = "gocardless-ruby/v#{GoCardless::VERSION}"
298
+ opts[:body] = MultiJson.encode(opts[:data]) if !opts[:data].nil?
299
+
300
+ # Reset the URL in case the environment / base URL has been changed.
301
+ @oauth_client.site = self.class.base_url
302
+
303
+ header_keys = opts[:headers].keys.map(&:to_s)
304
+ if header_keys.map(&:downcase).include?('authorization')
305
+ @oauth_client.request(method, path, opts)
306
+ else
307
+ @access_token.send(method, path, opts)
308
+ end
309
+ rescue OAuth2::Error => err
310
+ raise GoCardless::ApiError.new(err.response)
311
+ end
312
+
313
+ # Add a signature to a Hash of parameters. The signature will be generated
314
+ # from the app secret and the provided parameters, and should be used
315
+ # whenever signed data needs to be sent to GoCardless (e.g. when creating
316
+ # a new subscription). The signature will be added to the hash under the
317
+ # key +:signature+.
318
+ #
319
+ # @param [Hash] params the parameters to sign
320
+ # @return [Hash] the parameters with the new +:signature+ key
321
+ def sign_params(params)
322
+ params[:signature] = Utils.sign_params(params, @app_secret)
323
+ params
324
+ end
325
+
326
+ # Prepare a Hash of parameters for signing. Presence of required
327
+ # parameters is checked and the others are discarded.
328
+ #
329
+ # @param [Hash] params the parameters to be prepared for signing
330
+ # @return [Hash] the prepared parameters
331
+ def prepare_params(params)
332
+ # Create a new hash in case is a HashWithIndifferentAccess (keys are
333
+ # always a String)
334
+ params = Utils.symbolize_keys(Hash[params])
335
+ # Only pull out the relevant parameters, other won't be included in the
336
+ # signature so will cause false negatives
337
+ keys = [:resource_id, :resource_type, :resource_uri, :state, :signature]
338
+ params = Hash[params.select { |k,v| keys.include? k }]
339
+ (keys - [:state]).each do |key|
340
+ raise ArgumentError, "Parameters missing #{key}" if !params.key?(key)
341
+ end
342
+ params
343
+ end
344
+
345
+ # Check if a hash's :signature is valid
346
+ #
347
+ # @param [Hash] params the parameters to check
348
+ # @return [Boolean] whether or not the signature is valid
349
+ def signature_valid?(params)
350
+ params = params.clone
351
+ signature = params.delete(:signature)
352
+ sign_params(params)[:signature] == signature
353
+ end
354
+
355
+ # Generate a random base64-encoded string
356
+ #
357
+ # @return [String] a randomly generated string
358
+ def generate_nonce
359
+ Base64.encode64((0...45).map { rand(256).chr }.join).strip
360
+ end
361
+
362
+ # Generate the URL for creating a limit of type +type+, including the
363
+ # provided params, nonce, timestamp and signature
364
+ #
365
+ # @param [Symbol] type the limit type (+:subscription+, etc)
366
+ # @param [Hash] params the bill parameters
367
+ # @return [String] the generated URL
368
+ def new_limit_url(type, limit_params)
369
+ url = URI.parse("#{self.class.base_url}/connect/#{type}s/new")
370
+
371
+ limit_params[:merchant_id] = merchant_id
372
+ redirect_uri = limit_params.delete(:redirect_uri)
373
+ cancel_uri = limit_params.delete(:cancel_uri)
374
+ state = limit_params.delete(:state)
375
+
376
+ params = {
377
+ :nonce => generate_nonce,
378
+ :timestamp => Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ'),
379
+ :client_id => @app_id,
380
+ type => limit_params,
381
+ }
382
+ params[:redirect_uri] = redirect_uri unless redirect_uri.nil?
383
+ params[:cancel_uri] = cancel_uri unless cancel_uri.nil?
384
+ params[:state] = state unless state.nil?
385
+
386
+ sign_params(params)
387
+
388
+ url.query = Utils.normalize_params(params)
389
+ url.to_s
390
+ end
391
+ end
392
+ end
393
+