gocardless_pro 1.1.0 → 2.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.
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
@@ -26,7 +26,19 @@ module GoCardlessPro
26
26
  params = options.delete(:params) || {}
27
27
  options[:params] = {}
28
28
  options[:params][envelope_key] = params
29
- response = make_request(:post, path, options)
29
+
30
+ options[:retry_failures] = true
31
+
32
+ begin
33
+ response = make_request(:post, path, options)
34
+
35
+ # Response doesn't raise any errors until #body is called
36
+ response.tap(&:body)
37
+ rescue InvalidStateError => e
38
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
39
+
40
+ raise e
41
+ end
30
42
 
31
43
  return if response.body.nil?
32
44
 
@@ -40,7 +52,10 @@ module GoCardlessPro
40
52
  def list(options = {})
41
53
  path = '/payments'
42
54
 
55
+ options[:retry_failures] = true
56
+
43
57
  response = make_request(:get, path, options)
58
+
44
59
  ListResponse.new(
45
60
  response: response,
46
61
  unenveloped_body: unenvelope_body(response.body),
@@ -67,6 +82,8 @@ module GoCardlessPro
67
82
  def get(identity, options = {})
68
83
  path = sub_url('/payments/:identity', 'identity' => identity)
69
84
 
85
+ options[:retry_failures] = true
86
+
70
87
  response = make_request(:get, path, options)
71
88
 
72
89
  return if response.body.nil?
@@ -85,6 +102,9 @@ module GoCardlessPro
85
102
  params = options.delete(:params) || {}
86
103
  options[:params] = {}
87
104
  options[:params][envelope_key] = params
105
+
106
+ options[:retry_failures] = true
107
+
88
108
  response = make_request(:put, path, options)
89
109
 
90
110
  return if response.body.nil?
@@ -108,7 +128,19 @@ module GoCardlessPro
108
128
  params = options.delete(:params) || {}
109
129
  options[:params] = {}
110
130
  options[:params]['data'] = params
111
- response = make_request(:post, path, options)
131
+
132
+ options[:retry_failures] = false
133
+
134
+ begin
135
+ response = make_request(:post, path, options)
136
+
137
+ # Response doesn't raise any errors until #body is called
138
+ response.tap(&:body)
139
+ rescue InvalidStateError => e
140
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
141
+
142
+ raise e
143
+ end
112
144
 
113
145
  return if response.body.nil?
114
146
 
@@ -137,7 +169,19 @@ module GoCardlessPro
137
169
  params = options.delete(:params) || {}
138
170
  options[:params] = {}
139
171
  options[:params]['data'] = params
140
- response = make_request(:post, path, options)
172
+
173
+ options[:retry_failures] = false
174
+
175
+ begin
176
+ response = make_request(:post, path, options)
177
+
178
+ # Response doesn't raise any errors until #body is called
179
+ response.tap(&:body)
180
+ rescue InvalidStateError => e
181
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
182
+
183
+ raise e
184
+ end
141
185
 
142
186
  return if response.body.nil?
143
187
 
@@ -18,7 +18,10 @@ module GoCardlessPro
18
18
  def list(options = {})
19
19
  path = '/payouts'
20
20
 
21
+ options[:retry_failures] = true
22
+
21
23
  response = make_request(:get, path, options)
24
+
22
25
  ListResponse.new(
23
26
  response: response,
24
27
  unenveloped_body: unenvelope_body(response.body),
@@ -47,6 +50,8 @@ module GoCardlessPro
47
50
  def get(identity, options = {})
48
51
  path = sub_url('/payouts/:identity', 'identity' => identity)
49
52
 
53
+ options[:retry_failures] = true
54
+
50
55
  response = make_request(:get, path, options)
51
56
 
52
57
  return if response.body.nil?
@@ -21,7 +21,19 @@ module GoCardlessPro
21
21
  params = options.delete(:params) || {}
22
22
  options[:params] = {}
23
23
  options[:params][envelope_key] = params
24
- response = make_request(:post, path, options)
24
+
25
+ options[:retry_failures] = true
26
+
27
+ begin
28
+ response = make_request(:post, path, options)
29
+
30
+ # Response doesn't raise any errors until #body is called
31
+ response.tap(&:body)
32
+ rescue InvalidStateError => e
33
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
34
+
35
+ raise e
36
+ end
25
37
 
26
38
  return if response.body.nil?
27
39
 
@@ -36,6 +48,8 @@ module GoCardlessPro
36
48
  def get(identity, options = {})
37
49
  path = sub_url('/redirect_flows/:identity', 'identity' => identity)
38
50
 
51
+ options[:retry_failures] = true
52
+
39
53
  response = make_request(:get, path, options)
40
54
 
41
55
  return if response.body.nil?
@@ -64,7 +78,19 @@ module GoCardlessPro
64
78
  params = options.delete(:params) || {}
65
79
  options[:params] = {}
66
80
  options[:params]['data'] = params
67
- response = make_request(:post, path, options)
81
+
82
+ options[:retry_failures] = false
83
+
84
+ begin
85
+ response = make_request(:post, path, options)
86
+
87
+ # Response doesn't raise any errors until #body is called
88
+ response.tap(&:body)
89
+ rescue InvalidStateError => e
90
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
91
+
92
+ raise e
93
+ end
68
94
 
69
95
  return if response.body.nil?
70
96
 
@@ -38,7 +38,19 @@ module GoCardlessPro
38
38
  params = options.delete(:params) || {}
39
39
  options[:params] = {}
40
40
  options[:params][envelope_key] = params
41
- response = make_request(:post, path, options)
41
+
42
+ options[:retry_failures] = true
43
+
44
+ begin
45
+ response = make_request(:post, path, options)
46
+
47
+ # Response doesn't raise any errors until #body is called
48
+ response.tap(&:body)
49
+ rescue InvalidStateError => e
50
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
51
+
52
+ raise e
53
+ end
42
54
 
43
55
  return if response.body.nil?
44
56
 
@@ -52,7 +64,10 @@ module GoCardlessPro
52
64
  def list(options = {})
53
65
  path = '/refunds'
54
66
 
67
+ options[:retry_failures] = true
68
+
55
69
  response = make_request(:get, path, options)
70
+
56
71
  ListResponse.new(
57
72
  response: response,
58
73
  unenveloped_body: unenvelope_body(response.body),
@@ -79,6 +94,8 @@ module GoCardlessPro
79
94
  def get(identity, options = {})
80
95
  path = sub_url('/refunds/:identity', 'identity' => identity)
81
96
 
97
+ options[:retry_failures] = true
98
+
82
99
  response = make_request(:get, path, options)
83
100
 
84
101
  return if response.body.nil?
@@ -97,6 +114,9 @@ module GoCardlessPro
97
114
  params = options.delete(:params) || {}
98
115
  options[:params] = {}
99
116
  options[:params][envelope_key] = params
117
+
118
+ options[:retry_failures] = true
119
+
100
120
  response = make_request(:put, path, options)
101
121
 
102
122
  return if response.body.nil?
@@ -20,7 +20,19 @@ module GoCardlessPro
20
20
  params = options.delete(:params) || {}
21
21
  options[:params] = {}
22
22
  options[:params][envelope_key] = params
23
- response = make_request(:post, path, options)
23
+
24
+ options[:retry_failures] = true
25
+
26
+ begin
27
+ response = make_request(:post, path, options)
28
+
29
+ # Response doesn't raise any errors until #body is called
30
+ response.tap(&:body)
31
+ rescue InvalidStateError => e
32
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
33
+
34
+ raise e
35
+ end
24
36
 
25
37
  return if response.body.nil?
26
38
 
@@ -34,7 +46,10 @@ module GoCardlessPro
34
46
  def list(options = {})
35
47
  path = '/subscriptions'
36
48
 
49
+ options[:retry_failures] = true
50
+
37
51
  response = make_request(:get, path, options)
52
+
38
53
  ListResponse.new(
39
54
  response: response,
40
55
  unenveloped_body: unenvelope_body(response.body),
@@ -61,6 +76,8 @@ module GoCardlessPro
61
76
  def get(identity, options = {})
62
77
  path = sub_url('/subscriptions/:identity', 'identity' => identity)
63
78
 
79
+ options[:retry_failures] = true
80
+
64
81
  response = make_request(:get, path, options)
65
82
 
66
83
  return if response.body.nil?
@@ -79,6 +96,9 @@ module GoCardlessPro
79
96
  params = options.delete(:params) || {}
80
97
  options[:params] = {}
81
98
  options[:params][envelope_key] = params
99
+
100
+ options[:retry_failures] = true
101
+
82
102
  response = make_request(:put, path, options)
83
103
 
84
104
  return if response.body.nil?
@@ -102,7 +122,19 @@ module GoCardlessPro
102
122
  params = options.delete(:params) || {}
103
123
  options[:params] = {}
104
124
  options[:params]['data'] = params
105
- response = make_request(:post, path, options)
125
+
126
+ options[:retry_failures] = false
127
+
128
+ begin
129
+ response = make_request(:post, path, options)
130
+
131
+ # Response doesn't raise any errors until #body is called
132
+ response.tap(&:body)
133
+ rescue InvalidStateError => e
134
+ return get(e.conflicting_resource_id) if e.idempotent_creation_conflict?
135
+
136
+ raise e
137
+ end
106
138
 
107
139
  return if response.body.nil?
108
140
 
@@ -4,5 +4,5 @@ end
4
4
 
5
5
  module GoCardlessPro
6
6
  # Current version of the GC gem
7
- VERSION = '1.1.0'
7
+ VERSION = '2.0.0'
8
8
  end
@@ -3,9 +3,19 @@ require 'spec_helper'
3
3
  describe GoCardlessPro::ApiService do
4
4
  subject(:service) { described_class.new('https://api.example.com', 'secret_token') }
5
5
 
6
+ let(:default_response) do
7
+ {
8
+ status: 200,
9
+ body: '{}',
10
+ headers: { 'Content-Type' => 'application/json' }
11
+ }
12
+ end
13
+
6
14
  it 'uses basic auth' do
7
15
  stub = stub_request(:get, 'https://api.example.com/customers')
8
16
  .with(headers: { 'Authorization' => 'Bearer secret_token' })
17
+ .to_return(default_response)
18
+
9
19
  service.make_request(:get, '/customers')
10
20
  expect(stub).to have_been_requested
11
21
  end
@@ -13,6 +23,17 @@ describe GoCardlessPro::ApiService do
13
23
  describe 'making a get request without any parameters' do
14
24
  it 'is expected to call the correct stub' do
15
25
  stub = stub_request(:get, /.*api.example.com\/customers/)
26
+ .to_return(default_response)
27
+
28
+ service.make_request(:get, '/customers')
29
+ expect(stub).to have_been_requested
30
+ end
31
+
32
+ it "doesn't include an idempotency key" do
33
+ stub = stub_request(:get, /.*api.example.com\/customers/)
34
+ .with { |request| !request.headers.key?('Idempotency-Key') }
35
+ .to_return(default_response)
36
+
16
37
  service.make_request(:get, '/customers')
17
38
  expect(stub).to have_been_requested
18
39
  end
@@ -21,6 +42,17 @@ describe GoCardlessPro::ApiService do
21
42
  describe 'making a get request with query parameters' do
22
43
  it 'correctly passes the query parameters' do
23
44
  stub = stub_request(:get, /.*api.example.com\/customers\?a=1&b=2/)
45
+ .to_return(default_response)
46
+
47
+ service.make_request(:get, '/customers', params: { a: 1, b: 2 })
48
+ expect(stub).to have_been_requested
49
+ end
50
+
51
+ it "doesn't include an idempotency key" do
52
+ stub = stub_request(:get, /.*api.example.com\/customers\?a=1&b=2/)
53
+ .with { |request| !request.headers.key?('Idempotency-Key') }
54
+ .to_return(default_response)
55
+
24
56
  service.make_request(:get, '/customers', params: { a: 1, b: 2 })
25
57
  expect(stub).to have_been_requested
26
58
  end
@@ -30,6 +62,25 @@ describe GoCardlessPro::ApiService do
30
62
  it 'passes the data in as the post body' do
31
63
  stub = stub_request(:post, /.*api.example.com\/customers/)
32
64
  .with(body: { given_name: 'Jack', family_name: 'Franklin' })
65
+ .to_return(default_response)
66
+
67
+ service.make_request(:post, '/customers', params: {
68
+ given_name: 'Jack',
69
+ family_name: 'Franklin'
70
+ })
71
+ expect(stub).to have_been_requested
72
+ end
73
+
74
+ it 'generates a random idempotency key' do
75
+ allow(SecureRandom).to receive(:uuid).and_return('random-uuid')
76
+
77
+ stub = stub_request(:post, /.*api.example.com\/customers/)
78
+ .with(
79
+ body: { given_name: 'Jack', family_name: 'Franklin' },
80
+ headers: { 'Idempotency-Key' => 'random-uuid' }
81
+ )
82
+ .to_return(default_response)
83
+
33
84
  service.make_request(:post, '/customers', params: {
34
85
  given_name: 'Jack',
35
86
  family_name: 'Franklin'
@@ -45,6 +96,27 @@ describe GoCardlessPro::ApiService do
45
96
  body: { given_name: 'Jack', family_name: 'Franklin' },
46
97
  headers: { 'Foo' => 'Bar' }
47
98
  )
99
+ .to_return(default_response)
100
+
101
+ service.make_request(:post, '/customers', params: {
102
+ given_name: 'Jack',
103
+ family_name: 'Franklin'
104
+ },
105
+ headers: {
106
+ 'Foo' => 'Bar'
107
+ })
108
+ expect(stub).to have_been_requested
109
+ end
110
+
111
+ it 'merges in a random idempotency key' do
112
+ allow(SecureRandom).to receive(:uuid).and_return('random-uuid')
113
+
114
+ stub = stub_request(:post, /.*api.example.com\/customers/)
115
+ .with(
116
+ body: { given_name: 'Jack', family_name: 'Franklin' },
117
+ headers: { 'Idempotency-Key' => 'random-uuid', 'Foo' => 'Bar' }
118
+ )
119
+ .to_return(default_response)
48
120
 
49
121
  service.make_request(:post, '/customers', params: {
50
122
  given_name: 'Jack',
@@ -55,12 +127,46 @@ describe GoCardlessPro::ApiService do
55
127
  })
56
128
  expect(stub).to have_been_requested
57
129
  end
130
+
131
+ context 'with a custom idempotency key' do
132
+ it "doesn't replace it with a randomly-generated idempotency key" do
133
+ stub = stub_request(:post, /.*api.example.com\/customers/)
134
+ .with(
135
+ body: { given_name: 'Jack', family_name: 'Franklin' },
136
+ headers: { 'Idempotency-Key' => 'my-custom-idempotency-key' }
137
+ )
138
+ .to_return(default_response)
139
+
140
+ service.make_request(:post, '/customers', params: {
141
+ given_name: 'Jack',
142
+ family_name: 'Franklin'
143
+ },
144
+ headers: {
145
+ 'Idempotency-Key' => 'my-custom-idempotency-key'
146
+ })
147
+ expect(stub).to have_been_requested
148
+ end
149
+ end
58
150
  end
59
151
 
60
152
  describe 'making a put request with some data' do
61
153
  it 'passes the data in as the request body' do
62
154
  stub = stub_request(:put, /.*api.example.com\/customers\/CU123/)
63
155
  .with(body: { given_name: 'Jack', family_name: 'Franklin' })
156
+ .to_return(default_response)
157
+
158
+ service.make_request(:put, '/customers/CU123', params: {
159
+ given_name: 'Jack',
160
+ family_name: 'Franklin'
161
+ })
162
+ expect(stub).to have_been_requested
163
+ end
164
+
165
+ it "doesn't include an idempotency key" do
166
+ stub = stub_request(:put, /.*api.example.com\/customers\/CU123/)
167
+ .with { |request| !request.headers.key?('Idempotency-Key') }
168
+ .to_return(default_response)
169
+
64
170
  service.make_request(:put, '/customers/CU123', params: {
65
171
  given_name: 'Jack',
66
172
  family_name: 'Franklin'
@@ -0,0 +1,98 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoCardlessPro::Middlewares::RaiseGoCardlessErrors do
4
+ let(:connection) do
5
+ Faraday.new do |faraday|
6
+ faraday.response :raise_gocardless_errors
7
+ faraday.adapter :net_http
8
+ end
9
+ end
10
+
11
+ before do
12
+ stub_request(:post, 'https://api.gocardless.com/widgets').to_return(status: status,
13
+ body: body,
14
+ headers: headers)
15
+ end
16
+
17
+ let(:body) { nil }
18
+ let(:headers) { { 'Content-Type' => 'application/json' } }
19
+
20
+ context 'with a non-JSON response' do
21
+ let(:body) { '<html><body>Response from Cloudflare</body></html>' }
22
+ let(:headers) { { 'Content-Type' => 'text/html' } }
23
+ let(:status) { 514 }
24
+
25
+ it 'raises an error' do
26
+ expect { connection.post('https://api.gocardless.com/widgets') }
27
+ .to raise_error(GoCardlessPro::ApiError)
28
+ end
29
+ end
30
+
31
+ context 'with a 5XX response' do
32
+ let(:status) { 503 }
33
+
34
+ it 'raises an error' do
35
+ expect { connection.post('https://api.gocardless.com/widgets') }
36
+ .to raise_error(GoCardlessPro::ApiError)
37
+ end
38
+ end
39
+
40
+ context 'with a 2XX response' do
41
+ let(:status) { 200 }
42
+
43
+ it "doesn't raise an error" do
44
+ expect { connection.post('https://api.gocardless.com/widgets') }
45
+ .to_not raise_error(GoCardlessPro::ApiError)
46
+ end
47
+ end
48
+
49
+ context 'with a 4XX response' do
50
+ context 'for a validation error' do
51
+ let(:raw_response) do
52
+ double('response',
53
+ headers: default_headers,
54
+ status: 400,
55
+ body: { error: { type: 'validation_failed' } }.to_json
56
+ )
57
+ end
58
+
59
+ let(:status) { 422 }
60
+ let(:body) { { error: { type: 'validation_failed' } }.to_json }
61
+
62
+ it 'raises a ValidationError' do
63
+ expect { connection.post('https://api.gocardless.com/widgets') }
64
+ .to raise_error(GoCardlessPro::ValidationError)
65
+ end
66
+ end
67
+
68
+ context 'for a GoCardless error' do
69
+ let(:status) { 500 }
70
+ let(:body) { { error: { type: 'gocardless' } }.to_json }
71
+
72
+ it 'raises a GoCardlessError' do
73
+ expect { connection.post('https://api.gocardless.com/widgets') }
74
+ .to raise_error(GoCardlessPro::GoCardlessError)
75
+ end
76
+ end
77
+
78
+ context 'for an invalid API usage error' do
79
+ let(:status) { 400 }
80
+ let(:body) { { error: { type: 'invalid_api_usage' } }.to_json }
81
+
82
+ it 'raises a InvalidApiUsageError' do
83
+ expect { connection.post('https://api.gocardless.com/widgets') }
84
+ .to raise_error(GoCardlessPro::InvalidApiUsageError)
85
+ end
86
+ end
87
+
88
+ context 'for an invalid state error' do
89
+ let(:status) { 422 }
90
+ let(:body) { { error: { type: 'invalid_state' } }.to_json }
91
+
92
+ it 'raises an InvalidStateError' do
93
+ expect { connection.post('https://api.gocardless.com/widgets') }
94
+ .to raise_error(GoCardlessPro::InvalidStateError)
95
+ end
96
+ end
97
+ end
98
+ end