gocardless_pro 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -4
  3. data/lib/gocardless_pro.rb +1 -0
  4. data/lib/gocardless_pro/api_service.rb +2 -0
  5. data/lib/gocardless_pro/client.rb +4 -3
  6. data/lib/gocardless_pro/error/invalid_state_error.rb +17 -0
  7. data/lib/gocardless_pro/middlewares/raise_gocardless_errors.rb +50 -0
  8. data/lib/gocardless_pro/request.rb +38 -1
  9. data/lib/gocardless_pro/resources/creditor.rb +2 -2
  10. data/lib/gocardless_pro/resources/creditor_bank_account.rb +11 -11
  11. data/lib/gocardless_pro/resources/customer_bank_account.rb +2 -2
  12. data/lib/gocardless_pro/resources/event.rb +2 -2
  13. data/lib/gocardless_pro/resources/mandate.rb +2 -2
  14. data/lib/gocardless_pro/resources/payment.rb +7 -7
  15. data/lib/gocardless_pro/resources/payout.rb +7 -6
  16. data/lib/gocardless_pro/resources/redirect_flow.rb +2 -2
  17. data/lib/gocardless_pro/resources/refund.rb +2 -2
  18. data/lib/gocardless_pro/resources/subscription.rb +2 -2
  19. data/lib/gocardless_pro/response.rb +2 -54
  20. data/lib/gocardless_pro/services/bank_details_lookups_service.rb +3 -0
  21. data/lib/gocardless_pro/services/creditor_bank_accounts_service.rb +31 -2
  22. data/lib/gocardless_pro/services/creditors_service.rb +21 -1
  23. data/lib/gocardless_pro/services/customer_bank_accounts_service.rb +34 -2
  24. data/lib/gocardless_pro/services/customers_service.rb +21 -1
  25. data/lib/gocardless_pro/services/events_service.rb +5 -0
  26. data/lib/gocardless_pro/services/mandate_pdfs_service.rb +3 -0
  27. data/lib/gocardless_pro/services/mandates_service.rb +47 -3
  28. data/lib/gocardless_pro/services/payments_service.rb +47 -3
  29. data/lib/gocardless_pro/services/payouts_service.rb +5 -0
  30. data/lib/gocardless_pro/services/redirect_flows_service.rb +28 -2
  31. data/lib/gocardless_pro/services/refunds_service.rb +21 -1
  32. data/lib/gocardless_pro/services/subscriptions_service.rb +34 -2
  33. data/lib/gocardless_pro/version.rb +1 -1
  34. data/spec/api_service_spec.rb +106 -0
  35. data/spec/middlewares/raise_gocardless_errors_spec.rb +98 -0
  36. data/spec/resources/bank_details_lookup_spec.rb +102 -19
  37. data/spec/resources/creditor_bank_account_spec.rb +416 -40
  38. data/spec/resources/creditor_spec.rb +414 -53
  39. data/spec/resources/customer_bank_account_spec.rb +452 -40
  40. data/spec/resources/customer_spec.rb +457 -45
  41. data/spec/resources/event_spec.rb +171 -72
  42. data/spec/resources/mandate_pdf_spec.rb +100 -17
  43. data/spec/resources/mandate_spec.rb +501 -44
  44. data/spec/resources/payment_spec.rb +531 -48
  45. data/spec/resources/payout_spec.rb +189 -45
  46. data/spec/resources/redirect_flow_spec.rb +277 -43
  47. data/spec/resources/refund_spec.rb +349 -34
  48. data/spec/resources/subscription_spec.rb +531 -53
  49. data/spec/response_spec.rb +12 -79
  50. data/spec/services/bank_details_lookups_service_spec.rb +67 -2
  51. data/spec/services/creditor_bank_accounts_service_spec.rb +309 -31
  52. data/spec/services/creditors_service_spec.rb +343 -33
  53. data/spec/services/customer_bank_accounts_service_spec.rb +335 -32
  54. data/spec/services/customers_service_spec.rb +364 -36
  55. data/spec/services/events_service_spec.rb +185 -24
  56. data/spec/services/mandate_pdfs_service_spec.rb +66 -2
  57. data/spec/services/mandates_service_spec.rb +341 -33
  58. data/spec/services/payments_service_spec.rb +355 -35
  59. data/spec/services/payouts_service_spec.rb +206 -26
  60. data/spec/services/redirect_flows_service_spec.rb +137 -7
  61. data/spec/services/refunds_service_spec.rb +301 -27
  62. data/spec/services/subscriptions_service_spec.rb +377 -38
  63. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f9bf930f0861c969e5518b00a10e9751691b4505
4
- data.tar.gz: 3ca0cd94ef9b840cacf9e5aa3ddd7fb843404ed3
3
+ metadata.gz: b558be54b18a323263272b8629c8f7f57c5b034b
4
+ data.tar.gz: b7f920a9a2c6861183603d02bd19c803c245108f
5
5
  SHA512:
6
- metadata.gz: a86b8b85b3e57c6b199d8dbea5591bb3a393f339e2e506a939a82a2bdc22cb8df5361f1f4e4288604b29d3debdfde3b2a7ebb1d8895c640a456e4d795c3b5477
7
- data.tar.gz: 2bf60b407e6fdc07d2e582274197bff176b03f9fc4d30f25591943ab0479121ab9ea388c56c931d0bfe6d14aa5276d1e7e983faaa06fd212ed900ca84d83e4b3
6
+ metadata.gz: df5272b94fa8d97a0945fcc393e69a97558ce9dd4701c54049e6d4e66aea4b0521e3c3f2dc5783a3aebedb6f19a791e85fe40f016487e41726688f3341a72ac4
7
+ data.tar.gz: 42cc33ed47c7e8fa42f23d7e1b3e99bc7f19e7de0891adb82b9c62de102945982ceec33e7a2c5e111c9eabc9268c4a772d1d221bbd8938b4030f168c82a6ba9f
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  A Ruby client for the GoCardless API. For full details of the GoCardless API, see the [API docs](https://developer.gocardless.com/pro/).
4
4
 
5
- [![Gem Version](https://badge.fury.io/rb/statesman.png)](http://badge.fury.io/rb/gocardless_pro)
5
+ [![Gem Version](https://badge.fury.io/rb/gocardless_pro.svg)](http://badge.fury.io/rb/gocardless_pro)
6
6
  [![Build Status](https://travis-ci.org/gocardless/gocardless-pro-ruby.svg?branch=master)](https://travis-ci.org/gocardless/gocardless-pro-ruby)
7
7
 
8
8
  - ["Getting started" guide](https://developer.gocardless.com/getting-started/api/introduction/?lang=ruby) with copy and paste Ruby code samples
9
- - [API Reference](https://developer.gocardless.com/api-reference/2015-07-06)
9
+ - [API Reference](https://developer.gocardless.com/api-reference)
10
10
 
11
11
  ## Usage Examples
12
12
 
@@ -115,6 +115,11 @@ For POST and PUT requests you need to pass in the body in under the `params` key
115
115
  )
116
116
  ```
117
117
 
118
+ When creating a resource, the library will automatically include a randomly-generated
119
+ [idempotency key](https://developer.gocardless.com/api-reference/#making-requests-idempotency-keys)
120
+ - this means that if a request appears to fail but is in fact successful (for example due
121
+ to a timeout), you will not end up creating multiple duplicates of the resource.
122
+
118
123
  If any parameters are required they come first:
119
124
 
120
125
  ```rb
@@ -125,7 +130,7 @@ If any parameters are required they come first:
125
130
 
126
131
  Custom headers can be provided for a POST request under the `headers` key.
127
132
 
128
- The most common use of a custom header would be to set an [idempotency key](https://developer.gocardless.com/pro/#making-requests-idempotency-keys) when making a request:
133
+ The most common use of a custom header would be to set a custom [idempotency key](https://developer.gocardless.com/pro/#making-requests-idempotency-keys) when making a request:
129
134
 
130
135
  ```rb
131
136
  @client.customers.create(
@@ -142,7 +147,7 @@ The most common use of a custom header would be to set an [idempotency key](http
142
147
 
143
148
  ### Handling failures
144
149
 
145
- When the API returns an error, the client will raise a corresponding one. There are four classes of error which could be thrown, allof which subclass `GoCardlessPro::Error`:
150
+ When the API returns an error, the client will raise a corresponding one. There are four classes of error which could be thrown, all of which subclass `GoCardlessPro::Error`:
146
151
 
147
152
  - `GoCardlessPro::GoCardlessError`
148
153
  - `GoCardlessPro::InvalidApiUsageError`
@@ -151,6 +156,8 @@ When the API returns an error, the client will raise a corresponding one. There
151
156
 
152
157
  These errors are fully documented in the [API documentation](https://developer.gocardless.com/api-reference/#overview-errors).
153
158
 
159
+ When the API returns an `invalid_state` error due to an `idempotent_creation_conflict`, where possible, the library will automatically retrieve the existing record which was created using the idempotency key.
160
+
154
161
  All errors have the following methods to facilitate access to information in the API response:
155
162
 
156
163
  - `#documentation_url`
@@ -160,6 +167,8 @@ All errors have the following methods to facilitate access to information in the
160
167
  - `#request_id`
161
168
  - `#errors`
162
169
 
170
+ If a timeout occurs, and the request being made is idempotent, the library will automatically retry the request up to 3 times before giving up and raising a `Faraday::TimeoutError` error.
171
+
163
172
  ### Using the OAuth API
164
173
 
165
174
  The API includes [OAuth](https://developer.gocardless.com/pro/2015-07-06/#guides-oauth) functionality, which allows you to work with other users' GoCardless accounts. Once a user approves you, you can use the GoCardless API on their behalf and receive their webhooks.
@@ -24,6 +24,7 @@ end
24
24
 
25
25
  require_relative 'gocardless_pro/api_service'
26
26
  require_relative 'gocardless_pro/list_response'
27
+ require_relative 'gocardless_pro/middlewares/raise_gocardless_errors'
27
28
  require_relative 'gocardless_pro/error'
28
29
  require_relative 'gocardless_pro/error/validation_error'
29
30
  require_relative 'gocardless_pro/error/gocardless_error'
@@ -23,6 +23,8 @@ module GoCardlessPro
23
23
  connection_options = options[:connection_options]
24
24
 
25
25
  @connection = Faraday.new(root_url, connection_options) do |faraday|
26
+ faraday.response :raise_gocardless_errors
27
+
26
28
  faraday.adapter(*http_adapter)
27
29
  end
28
30
 
@@ -116,7 +116,9 @@ module GoCardlessPro
116
116
  default_headers: {
117
117
  'GoCardless-Version' => '2015-07-06',
118
118
  'User-Agent' => "#{user_agent}",
119
- 'Content-Type' => 'application/json'
119
+ 'Content-Type' => 'application/json',
120
+ 'GoCardless-Client-Library' => 'gocardless-pro-ruby',
121
+ 'GoCardless-Client-Version' => '2.0.0'
120
122
  }
121
123
  }
122
124
  end
@@ -124,8 +126,7 @@ module GoCardlessPro
124
126
  def user_agent
125
127
  @user_agent ||=
126
128
  begin
127
- gem_name = 'gocardless_pro'
128
- gem_info = "#{gem_name}"
129
+ gem_info = 'gocardless-pro-ruby'
129
130
  gem_info += "/v#{GoCardlessPro::VERSION}" if defined?(GoCardlessPro::VERSION)
130
131
 
131
132
  ruby_engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'
@@ -1,5 +1,22 @@
1
1
  module GoCardlessPro
2
2
  # Thrown when the API returns an invalid state error
3
3
  class InvalidStateError < Error
4
+ IDEMPOTENT_CREATION_CONFLICT = 'idempotent_creation_conflict'.freeze
5
+ CONFLICTING_RESOURCE_ID = 'conflicting_resource_id'.freeze
6
+
7
+ def idempotent_creation_conflict?
8
+ !idempotent_creation_conflict_error.nil?
9
+ end
10
+
11
+ def conflicting_resource_id
12
+ return unless idempotent_creation_conflict?
13
+ idempotent_creation_conflict_error['links'][CONFLICTING_RESOURCE_ID]
14
+ end
15
+
16
+ private
17
+
18
+ def idempotent_creation_conflict_error
19
+ errors.find { |error| error['reason'] == IDEMPOTENT_CREATION_CONFLICT }
20
+ end
4
21
  end
5
22
  end
@@ -0,0 +1,50 @@
1
+ module GoCardlessPro
2
+ module Middlewares
3
+ class RaiseGoCardlessErrors < Faraday::Response::Middleware
4
+ API_ERROR_STATUSES = 501..599
5
+ CLIENT_ERROR_STATUSES = 400..500
6
+
7
+ def on_complete(env)
8
+ if !json?(env) || API_ERROR_STATUSES.include?(env.status)
9
+ fail ApiError, generate_error_data(env)
10
+ end
11
+
12
+ if CLIENT_ERROR_STATUSES.include?(env.status)
13
+ json_body ||= JSON.parse(env.body) unless env.body.empty?
14
+ error_type = json_body['error']['type']
15
+ fail(error_class_for_type(error_type), json_body['error'])
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def error_class_for_type(type)
22
+ {
23
+ validation_failed: GoCardlessPro::ValidationError,
24
+ gocardless: GoCardlessPro::GoCardlessError,
25
+ invalid_api_usage: GoCardlessPro::InvalidApiUsageError,
26
+ invalid_state: GoCardlessPro::InvalidStateError
27
+ }.fetch(type.to_sym)
28
+ end
29
+
30
+ def generate_error_data(env)
31
+ {
32
+ 'message' => "Something went wrong with this request\n" \
33
+ "code: #{env.status}\n" \
34
+ "headers: #{env.response_headers}\n" \
35
+ "body: #{env.body}",
36
+ 'code' => env.status
37
+ }
38
+ end
39
+
40
+ def json?(env)
41
+ content_type = env.response_headers['Content-Type'] ||
42
+ env.response_headers['content-type'] || ''
43
+
44
+ content_type.include?('application/json')
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ Faraday::Response.register_middleware raise_gocardless_errors: GoCardlessPro::Middlewares::RaiseGoCardlessErrors
@@ -1,6 +1,11 @@
1
+ require 'securerandom'
2
+
1
3
  module GoCardlessPro
2
4
  # A class that wraps an API request
3
5
  class Request
6
+ MAX_NETWORK_RETRIES = 3
7
+ RETRY_DELAY = 0.5
8
+
4
9
  # Initialize a request class, which makes calls to the API
5
10
  # @param connection
6
11
  # @param method [Symbol] the method to make the request with
@@ -13,6 +18,7 @@ module GoCardlessPro
13
18
  @path = path
14
19
  @headers = options.delete(:headers) || {}
15
20
  @envelope_name = options.delete(:envelope_key)
21
+ @retry_failures = options.delete(:retry_failures) { true }
16
22
  @given_options = options
17
23
 
18
24
  @request_body = request_body
@@ -21,11 +27,34 @@ module GoCardlessPro
21
27
  @request_body = @request_body.to_json
22
28
  @headers['Content-Type'] ||= 'application/json'
23
29
  end
30
+
31
+ @headers['Idempotency-Key'] ||= SecureRandom.uuid if @method == :post
24
32
  end
25
33
 
26
34
  # Make the request and wrap it in a Response object
27
35
  def request
28
- Response.new(make_request)
36
+ if @retry_failures
37
+ with_retries { Response.new(make_request) }
38
+ else
39
+ Response.new(make_request)
40
+ end
41
+ end
42
+
43
+ def with_retries
44
+ requests_attempted = 0
45
+ total_requests_allowed = MAX_NETWORK_RETRIES + 1
46
+
47
+ begin
48
+ yield
49
+ rescue => exception
50
+ if requests_attempted < total_requests_allowed && should_retry?(exception)
51
+ requests_attempted += 1
52
+ sleep(RETRY_DELAY)
53
+ retry
54
+ else
55
+ raise exception
56
+ end
57
+ end
29
58
  end
30
59
 
31
60
  # Make the API request
@@ -57,5 +86,13 @@ module GoCardlessPro
57
86
  {}
58
87
  end
59
88
  end
89
+
90
+ private
91
+
92
+ def should_retry?(exception)
93
+ return true if exception.is_a?(Faraday::TimeoutError)
94
+ return true if exception.is_a?(Faraday::ConnectionFailed)
95
+ return true if exception.is_a?(GoCardlessPro::ApiError)
96
+ end
60
97
  end
61
98
  end
@@ -61,7 +61,7 @@ module GoCardlessPro
61
61
 
62
62
  # Return the links that the resource has
63
63
  def links
64
- @links_links ||= Links.new(@links)
64
+ @creditor_links ||= Links.new(@links)
65
65
  end
66
66
 
67
67
  # Provides the creditor resource as a hash of all its readable attributes
@@ -71,7 +71,7 @@ module GoCardlessPro
71
71
 
72
72
  class Links
73
73
  def initialize(links)
74
- @links = links
74
+ @links = links || {}
75
75
  end
76
76
 
77
77
  def default_eur_payout_account
@@ -12,15 +12,15 @@ module GoCardlessPro
12
12
  # Represents an instance of a creditor_bank_account resource returned from the API
13
13
 
14
14
  # Creditor Bank Accounts hold the bank details of a
15
- # [creditor](#whitelabel-partner-endpoints-creditors). These are the bank
16
- # accounts which your [payouts](#core-endpoints-payouts) will be sent to.
17
-
18
- # #
19
- # Note that creditor bank accounts must be unique, and so you will
20
- # encounter a `bank_account_exists` error if you try to create a duplicate
21
- # bank account. You may wish to handle this by updating the existing record
22
- # instead, the ID of which will be provided as
23
- # `links[creditor_bank_account]` in the error response.
15
+ # [creditor](#core-endpoints-creditors). These are the bank accounts which
16
+ # your [payouts](#core-endpoints-payouts) will be sent to.
17
+ #
18
+ # Note
19
+ # that creditor bank accounts must be unique, and so you will encounter a
20
+ # `bank_account_exists` error if you try to create a duplicate bank account.
21
+ # You may wish to handle this by updating the existing record instead, the
22
+ # ID of which will be provided as `links[creditor_bank_account]` in the
23
+ # error response.
24
24
  class CreditorBankAccount
25
25
  attr_reader :account_holder_name
26
26
  attr_reader :account_number_ending
@@ -56,7 +56,7 @@ module GoCardlessPro
56
56
 
57
57
  # Return the links that the resource has
58
58
  def links
59
- @links_links ||= Links.new(@links)
59
+ @creditor_bank_account_links ||= Links.new(@links)
60
60
  end
61
61
 
62
62
  # Provides the creditor_bank_account resource as a hash of all its readable attributes
@@ -66,7 +66,7 @@ module GoCardlessPro
66
66
 
67
67
  class Links
68
68
  def initialize(links)
69
- @links = links
69
+ @links = links || {}
70
70
  end
71
71
 
72
72
  def creditor
@@ -57,7 +57,7 @@ module GoCardlessPro
57
57
 
58
58
  # Return the links that the resource has
59
59
  def links
60
- @links_links ||= Links.new(@links)
60
+ @customer_bank_account_links ||= Links.new(@links)
61
61
  end
62
62
 
63
63
  # Provides the customer_bank_account resource as a hash of all its readable attributes
@@ -67,7 +67,7 @@ module GoCardlessPro
67
67
 
68
68
  class Links
69
69
  def initialize(links)
70
- @links = links
70
+ @links = links || {}
71
71
  end
72
72
 
73
73
  def customer
@@ -43,7 +43,7 @@ module GoCardlessPro
43
43
 
44
44
  # Return the links that the resource has
45
45
  def links
46
- @links_links ||= Links.new(@links)
46
+ @event_links ||= Links.new(@links)
47
47
  end
48
48
 
49
49
  # Provides the event resource as a hash of all its readable attributes
@@ -53,7 +53,7 @@ module GoCardlessPro
53
53
 
54
54
  class Links
55
55
  def initialize(links)
56
- @links = links
56
+ @links = links || {}
57
57
  end
58
58
 
59
59
  def mandate
@@ -50,7 +50,7 @@ module GoCardlessPro
50
50
 
51
51
  # Return the links that the resource has
52
52
  def links
53
- @links_links ||= Links.new(@links)
53
+ @mandate_links ||= Links.new(@links)
54
54
  end
55
55
 
56
56
  # Provides the mandate resource as a hash of all its readable attributes
@@ -60,7 +60,7 @@ module GoCardlessPro
60
60
 
61
61
  class Links
62
62
  def initialize(links)
63
- @links = links
63
+ @links = links || {}
64
64
  end
65
65
 
66
66
  def creditor
@@ -13,12 +13,12 @@ module GoCardlessPro
13
13
 
14
14
  # Payment objects represent payments from a
15
15
  # [customer](#core-endpoints-customers) to a
16
- # [creditor](#whitelabel-partner-endpoints-creditors), taken against a
17
- # Direct Debit [mandate](#core-endpoints-mandates).
16
+ # [creditor](#core-endpoints-creditors), taken against a Direct Debit
17
+ # [mandate](#core-endpoints-mandates).
18
18
  #
19
- # GoCardless
20
- # will notify you via a [webhook](#appendix-webhooks) whenever the state of
21
- # a payment changes.
19
+ # GoCardless will notify
20
+ # you via a [webhook](#appendix-webhooks) whenever the state of a payment
21
+ # changes.
22
22
  class Payment
23
23
  attr_reader :amount
24
24
  attr_reader :amount_refunded
@@ -56,7 +56,7 @@ module GoCardlessPro
56
56
 
57
57
  # Return the links that the resource has
58
58
  def links
59
- @links_links ||= Links.new(@links)
59
+ @payment_links ||= Links.new(@links)
60
60
  end
61
61
 
62
62
  # Provides the payment resource as a hash of all its readable attributes
@@ -66,7 +66,7 @@ module GoCardlessPro
66
66
 
67
67
  class Links
68
68
  def initialize(links)
69
- @links = links
69
+ @links = links || {}
70
70
  end
71
71
 
72
72
  def creditor
@@ -12,10 +12,9 @@ module GoCardlessPro
12
12
  # Represents an instance of a payout resource returned from the API
13
13
 
14
14
  # Payouts represent transfers from GoCardless to a
15
- # [creditor](#whitelabel-partner-endpoints-creditors). Each payout contains
16
- # the funds collected from one or many [payments](#core-endpoints-payments).
17
- # Payouts are created automatically after a payment has been successfully
18
- # collected.
15
+ # [creditor](#core-endpoints-creditors). Each payout contains the funds
16
+ # collected from one or many [payments](#core-endpoints-payments). Payouts
17
+ # are created automatically after a payment has been successfully collected.
19
18
  class Payout
20
19
  attr_reader :amount
21
20
  attr_reader :arrival_date
@@ -23,6 +22,7 @@ module GoCardlessPro
23
22
  attr_reader :currency
24
23
  attr_reader :deducted_fees
25
24
  attr_reader :id
25
+ attr_reader :payout_type
26
26
  attr_reader :reference
27
27
  attr_reader :status
28
28
 
@@ -38,6 +38,7 @@ module GoCardlessPro
38
38
  @deducted_fees = object['deducted_fees']
39
39
  @id = object['id']
40
40
  @links = object['links']
41
+ @payout_type = object['payout_type']
41
42
  @reference = object['reference']
42
43
  @status = object['status']
43
44
  @response = response
@@ -49,7 +50,7 @@ module GoCardlessPro
49
50
 
50
51
  # Return the links that the resource has
51
52
  def links
52
- @links_links ||= Links.new(@links)
53
+ @payout_links ||= Links.new(@links)
53
54
  end
54
55
 
55
56
  # Provides the payout resource as a hash of all its readable attributes
@@ -59,7 +60,7 @@ module GoCardlessPro
59
60
 
60
61
  class Links
61
62
  def initialize(links)
62
- @links = links
63
+ @links = links || {}
63
64
  end
64
65
 
65
66
  def creditor