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