gocardless_pro 2.17.1 → 2.21.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/lib/gocardless_pro.rb +3 -0
  3. data/lib/gocardless_pro/client.rb +6 -1
  4. data/lib/gocardless_pro/resources/creditor_bank_account.rb +1 -2
  5. data/lib/gocardless_pro/resources/currency_exchange_rate.rb +44 -0
  6. data/lib/gocardless_pro/resources/customer_notification.rb +3 -5
  7. data/lib/gocardless_pro/resources/event.rb +2 -1
  8. data/lib/gocardless_pro/resources/mandate_import.rb +5 -8
  9. data/lib/gocardless_pro/resources/mandate_import_entry.rb +3 -5
  10. data/lib/gocardless_pro/resources/payout.rb +2 -0
  11. data/lib/gocardless_pro/resources/redirect_flow.rb +2 -0
  12. data/lib/gocardless_pro/resources/subscription.rb +38 -29
  13. data/lib/gocardless_pro/services/currency_exchange_rates_service.rb +67 -0
  14. data/lib/gocardless_pro/services/customers_service.rb +1 -2
  15. data/lib/gocardless_pro/services/instalment_schedules_service.rb +81 -5
  16. data/lib/gocardless_pro/services/mandates_service.rb +1 -1
  17. data/lib/gocardless_pro/services/payouts_service.rb +21 -0
  18. data/lib/gocardless_pro/services/subscriptions_service.rb +129 -0
  19. data/lib/gocardless_pro/version.rb +1 -1
  20. data/spec/resources/currency_exchange_rate_spec.rb +103 -0
  21. data/spec/resources/instalment_schedule_spec.rb +193 -1
  22. data/spec/resources/payout_spec.rb +45 -0
  23. data/spec/resources/redirect_flow_spec.rb +9 -0
  24. data/spec/resources/subscription_spec.rb +210 -0
  25. data/spec/services/currency_exchange_rates_service_spec.rb +223 -0
  26. data/spec/services/instalment_schedules_service_spec.rb +270 -1
  27. data/spec/services/payouts_service_spec.rb +74 -0
  28. data/spec/services/redirect_flows_service_spec.rb +9 -0
  29. data/spec/services/subscriptions_service_spec.rb +240 -0
  30. metadata +9 -3
@@ -172,7 +172,7 @@ module GoCardlessPro
172
172
  # This will fail with a `mandate_not_inactive` error if the mandate is already
173
173
  # being submitted, or is active.
174
174
  #
175
- # Mandates can be resubmitted up to 3 times.
175
+ # Mandates can be resubmitted up to 10 times.
176
176
  # Example URL: /mandates/:identity/actions/reinstate
177
177
  #
178
178
  # @param identity # Unique identifier, beginning with "MD". Note that this prefix may not
@@ -59,6 +59,27 @@ module GoCardlessPro
59
59
  Resources::Payout.new(unenvelope_body(response.body), response)
60
60
  end
61
61
 
62
+ # Updates a payout object. This accepts only the metadata parameter.
63
+ # Example URL: /payouts/:identity
64
+ #
65
+ # @param identity # Unique identifier, beginning with "PO".
66
+ # @param options [Hash] parameters as a hash, under a params key.
67
+ def update(identity, options = {})
68
+ path = sub_url('/payouts/:identity', 'identity' => identity)
69
+
70
+ params = options.delete(:params) || {}
71
+ options[:params] = {}
72
+ options[:params][envelope_key] = params
73
+
74
+ options[:retry_failures] = true
75
+
76
+ response = make_request(:put, path, options)
77
+
78
+ return if response.body.nil?
79
+
80
+ Resources::Payout.new(unenvelope_body(response.body), response)
81
+ end
82
+
62
83
  private
63
84
 
64
85
  # Unenvelope the response of the body using the service's `envelope_key`
@@ -138,6 +138,135 @@ module GoCardlessPro
138
138
  Resources::Subscription.new(unenvelope_body(response.body), response)
139
139
  end
140
140
 
141
+ # Pause a subscription object.
142
+ # No payments will be created until it is resumed.
143
+ #
144
+ # This can only be used when a subscription collecting a fixed number of
145
+ # payments (created using `count`)
146
+ # or when they continue forever (created without `count` or `end_date`)
147
+ #
148
+ # When `pause_cycles` is omitted the subscription is paused until the [resume
149
+ # endpoint](#subscriptions-resume-a-subscription) is called.
150
+ # If the subscription is collecting a fixed number of payments, `end_date` will
151
+ # be set to `nil`.
152
+ # When paused indefinitely, `upcoming_payments` will be empty.
153
+ #
154
+ # When `pause_cycles` is provided the subscription will be paused for the number
155
+ # of cycles requested.
156
+ # If the subscription is collecting a fixed number of payments, `end_date` will
157
+ # be set to a new value.
158
+ # When paused for a number of cycles, `upcoming_payments` will still contain the
159
+ # upcoming charge dates.
160
+ #
161
+ # This fails with:
162
+ #
163
+ # - `forbidden` if the subscription was created by an app and you are not
164
+ # authenticated as that app, or if the subscription was not created by an app
165
+ # and you are authenticated as an app
166
+ #
167
+ # - `validation_failed` if invalid data is provided when attempting to pause a
168
+ # subscription.
169
+ #
170
+ # - `subscription_not_active` if the subscription is no longer active.
171
+ #
172
+ # - `subscription_already_ended` if the subscription has taken all payments.
173
+ #
174
+ # - `pause_cycles_must_be_greater_than_or_equal_to` if the provided value for
175
+ # `pause_cycles` cannot be satisfied.
176
+ #
177
+ # Example URL: /subscriptions/:identity/actions/pause
178
+ #
179
+ # @param identity # Unique identifier, beginning with "SB".
180
+ # @param options [Hash] parameters as a hash, under a params key.
181
+ def pause(identity, options = {})
182
+ path = sub_url('/subscriptions/:identity/actions/pause', 'identity' => identity)
183
+
184
+ params = options.delete(:params) || {}
185
+ options[:params] = {}
186
+ options[:params]['data'] = params
187
+
188
+ options[:retry_failures] = false
189
+
190
+ begin
191
+ response = make_request(:post, path, options)
192
+
193
+ # Response doesn't raise any errors until #body is called
194
+ response.tap(&:body)
195
+ rescue InvalidStateError => e
196
+ if e.idempotent_creation_conflict?
197
+ case @api_service.on_idempotency_conflict
198
+ when :raise
199
+ raise IdempotencyConflict, e.error
200
+ when :fetch
201
+ return get(e.conflicting_resource_id)
202
+ else
203
+ raise ArgumentError, 'Unknown mode for :on_idempotency_conflict'
204
+ end
205
+ end
206
+
207
+ raise e
208
+ end
209
+
210
+ return if response.body.nil?
211
+
212
+ Resources::Subscription.new(unenvelope_body(response.body), response)
213
+ end
214
+
215
+ # Resume a subscription object.
216
+ # Payments will start to be created again based on the subscriptions recurrence
217
+ # rules.
218
+ # The `charge_date` on the next payment will be the same as the subscriptions
219
+ # `earliest_charge_date_after_resume`
220
+ #
221
+ # This fails with:
222
+ #
223
+ # - `forbidden` if the subscription was created by an app and you are not
224
+ # authenticated as that app, or if the subscription was not created by an app
225
+ # and you are authenticated as an app
226
+ #
227
+ # - `validation_failed` if invalid data is provided when attempting to resume a
228
+ # subscription.
229
+ #
230
+ # - `subscription_not_paused` if the subscription is not paused.
231
+ #
232
+ # Example URL: /subscriptions/:identity/actions/resume
233
+ #
234
+ # @param identity # Unique identifier, beginning with "SB".
235
+ # @param options [Hash] parameters as a hash, under a params key.
236
+ def resume(identity, options = {})
237
+ path = sub_url('/subscriptions/:identity/actions/resume', 'identity' => identity)
238
+
239
+ params = options.delete(:params) || {}
240
+ options[:params] = {}
241
+ options[:params]['data'] = params
242
+
243
+ options[:retry_failures] = false
244
+
245
+ begin
246
+ response = make_request(:post, path, options)
247
+
248
+ # Response doesn't raise any errors until #body is called
249
+ response.tap(&:body)
250
+ rescue InvalidStateError => e
251
+ if e.idempotent_creation_conflict?
252
+ case @api_service.on_idempotency_conflict
253
+ when :raise
254
+ raise IdempotencyConflict, e.error
255
+ when :fetch
256
+ return get(e.conflicting_resource_id)
257
+ else
258
+ raise ArgumentError, 'Unknown mode for :on_idempotency_conflict'
259
+ end
260
+ end
261
+
262
+ raise e
263
+ end
264
+
265
+ return if response.body.nil?
266
+
267
+ Resources::Subscription.new(unenvelope_body(response.body), response)
268
+ end
269
+
141
270
  # Immediately cancels a subscription; no more payments will be created under it.
142
271
  # Any metadata supplied to this endpoint will be stored on the payment
143
272
  # cancellation event it causes.
@@ -4,5 +4,5 @@ end
4
4
 
5
5
  module GoCardlessPro
6
6
  # Current version of the GC gem
7
- VERSION = '2.17.1'.freeze
7
+ VERSION = '2.21.0'.freeze
8
8
  end
@@ -0,0 +1,103 @@
1
+ require 'spec_helper'
2
+
3
+ describe GoCardlessPro::Resources::CurrencyExchangeRate do
4
+ let(:client) do
5
+ GoCardlessPro::Client.new(
6
+ access_token: 'SECRET_TOKEN'
7
+ )
8
+ end
9
+
10
+ let(:response_headers) { { 'Content-Type' => 'application/json' } }
11
+
12
+ describe '#list' do
13
+ describe 'with no filters' do
14
+ subject(:get_list_response) { client.currency_exchange_rates.list }
15
+
16
+ before do
17
+ stub_request(:get, %r{.*api.gocardless.com/currency_exchange_rates}).to_return(
18
+ body: {
19
+ 'currency_exchange_rates' => [{
20
+
21
+ 'rate' => 'rate-input',
22
+ 'source' => 'source-input',
23
+ 'target' => 'target-input',
24
+ 'time' => 'time-input',
25
+ }],
26
+ meta: {
27
+ cursors: {
28
+ before: nil,
29
+ after: 'ABC123',
30
+ },
31
+ },
32
+ }.to_json,
33
+ headers: response_headers
34
+ )
35
+ end
36
+
37
+ it 'wraps each item in the resource class' do
38
+ expect(get_list_response.records.map(&:class).uniq.first).to eq(GoCardlessPro::Resources::CurrencyExchangeRate)
39
+
40
+ expect(get_list_response.records.first.rate).to eq('rate-input')
41
+
42
+ expect(get_list_response.records.first.source).to eq('source-input')
43
+
44
+ expect(get_list_response.records.first.target).to eq('target-input')
45
+
46
+ expect(get_list_response.records.first.time).to eq('time-input')
47
+ end
48
+
49
+ it 'exposes the cursors for before and after' do
50
+ expect(get_list_response.before).to eq(nil)
51
+ expect(get_list_response.after).to eq('ABC123')
52
+ end
53
+
54
+ specify { expect(get_list_response.api_response.headers).to eql('content-type' => 'application/json') }
55
+ end
56
+ end
57
+
58
+ describe '#all' do
59
+ let!(:first_response_stub) do
60
+ stub_request(:get, %r{.*api.gocardless.com/currency_exchange_rates$}).to_return(
61
+ body: {
62
+ 'currency_exchange_rates' => [{
63
+
64
+ 'rate' => 'rate-input',
65
+ 'source' => 'source-input',
66
+ 'target' => 'target-input',
67
+ 'time' => 'time-input',
68
+ }],
69
+ meta: {
70
+ cursors: { after: 'AB345' },
71
+ limit: 1,
72
+ },
73
+ }.to_json,
74
+ headers: response_headers
75
+ )
76
+ end
77
+
78
+ let!(:second_response_stub) do
79
+ stub_request(:get, %r{.*api.gocardless.com/currency_exchange_rates\?after=AB345}).to_return(
80
+ body: {
81
+ 'currency_exchange_rates' => [{
82
+
83
+ 'rate' => 'rate-input',
84
+ 'source' => 'source-input',
85
+ 'target' => 'target-input',
86
+ 'time' => 'time-input',
87
+ }],
88
+ meta: {
89
+ limit: 2,
90
+ cursors: {},
91
+ },
92
+ }.to_json,
93
+ headers: response_headers
94
+ )
95
+ end
96
+
97
+ it 'automatically makes the extra requests' do
98
+ expect(client.currency_exchange_rates.all.to_a.length).to eq(2)
99
+ expect(first_response_stub).to have_been_requested
100
+ expect(second_response_stub).to have_been_requested
101
+ end
102
+ end
103
+ end
@@ -10,7 +10,164 @@ describe GoCardlessPro::Resources::InstalmentSchedule do
10
10
  let(:response_headers) { { 'Content-Type' => 'application/json' } }
11
11
 
12
12
  describe '#create' do
13
- subject(:post_create_response) { client.instalment_schedules.create(params: new_resource) }
13
+ subject(:post_create_response) { client.instalment_schedules.create_with_dates(params: new_resource) }
14
+ context 'with a valid request' do
15
+ let(:new_resource) do
16
+ {
17
+
18
+ 'created_at' => 'created_at-input',
19
+ 'currency' => 'currency-input',
20
+ 'id' => 'id-input',
21
+ 'links' => 'links-input',
22
+ 'metadata' => 'metadata-input',
23
+ 'name' => 'name-input',
24
+ 'payment_errors' => 'payment_errors-input',
25
+ 'status' => 'status-input',
26
+ 'total_amount' => 'total_amount-input',
27
+ }
28
+ end
29
+
30
+ before do
31
+ stub_request(:post, %r{.*api.gocardless.com/instalment_schedules}).
32
+ with(
33
+ body: {
34
+ 'instalment_schedules' => {
35
+
36
+ 'created_at' => 'created_at-input',
37
+ 'currency' => 'currency-input',
38
+ 'id' => 'id-input',
39
+ 'links' => 'links-input',
40
+ 'metadata' => 'metadata-input',
41
+ 'name' => 'name-input',
42
+ 'payment_errors' => 'payment_errors-input',
43
+ 'status' => 'status-input',
44
+ 'total_amount' => 'total_amount-input',
45
+ },
46
+ }
47
+ ).
48
+ to_return(
49
+ body: {
50
+ 'instalment_schedules' =>
51
+
52
+ {
53
+
54
+ 'created_at' => 'created_at-input',
55
+ 'currency' => 'currency-input',
56
+ 'id' => 'id-input',
57
+ 'links' => 'links-input',
58
+ 'metadata' => 'metadata-input',
59
+ 'name' => 'name-input',
60
+ 'payment_errors' => 'payment_errors-input',
61
+ 'status' => 'status-input',
62
+ 'total_amount' => 'total_amount-input',
63
+ },
64
+
65
+ }.to_json,
66
+ headers: response_headers
67
+ )
68
+ end
69
+
70
+ it 'creates and returns the resource' do
71
+ expect(post_create_response).to be_a(GoCardlessPro::Resources::InstalmentSchedule)
72
+ end
73
+ end
74
+
75
+ context 'with a request that returns a validation error' do
76
+ let(:new_resource) { {} }
77
+
78
+ before do
79
+ stub_request(:post, %r{.*api.gocardless.com/instalment_schedules}).to_return(
80
+ body: {
81
+ error: {
82
+ type: 'validation_failed',
83
+ code: 422,
84
+ errors: [
85
+ { message: 'test error message', field: 'test_field' },
86
+ ],
87
+ },
88
+ }.to_json,
89
+ headers: response_headers,
90
+ status: 422
91
+ )
92
+ end
93
+
94
+ it 'throws the correct error' do
95
+ expect { post_create_response }.to raise_error(GoCardlessPro::ValidationError)
96
+ end
97
+ end
98
+
99
+ context 'with a request that returns an idempotent creation conflict error' do
100
+ let(:id) { 'ID123' }
101
+
102
+ let(:new_resource) do
103
+ {
104
+
105
+ 'created_at' => 'created_at-input',
106
+ 'currency' => 'currency-input',
107
+ 'id' => 'id-input',
108
+ 'links' => 'links-input',
109
+ 'metadata' => 'metadata-input',
110
+ 'name' => 'name-input',
111
+ 'payment_errors' => 'payment_errors-input',
112
+ 'status' => 'status-input',
113
+ 'total_amount' => 'total_amount-input',
114
+ }
115
+ end
116
+
117
+ let!(:post_stub) do
118
+ stub_request(:post, %r{.*api.gocardless.com/instalment_schedules}).to_return(
119
+ body: {
120
+ error: {
121
+ type: 'invalid_state',
122
+ code: 409,
123
+ errors: [
124
+ {
125
+ message: 'A resource has already been created with this idempotency key',
126
+ reason: 'idempotent_creation_conflict',
127
+ links: {
128
+ conflicting_resource_id: id,
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ }.to_json,
134
+ headers: response_headers,
135
+ status: 409
136
+ )
137
+ end
138
+
139
+ let!(:get_stub) do
140
+ stub_url = "/instalment_schedules/#{id}"
141
+ stub_request(:get, /.*api.gocardless.com#{stub_url}/).
142
+ to_return(
143
+ body: {
144
+ 'instalment_schedules' => {
145
+
146
+ 'created_at' => 'created_at-input',
147
+ 'currency' => 'currency-input',
148
+ 'id' => 'id-input',
149
+ 'links' => 'links-input',
150
+ 'metadata' => 'metadata-input',
151
+ 'name' => 'name-input',
152
+ 'payment_errors' => 'payment_errors-input',
153
+ 'status' => 'status-input',
154
+ 'total_amount' => 'total_amount-input',
155
+ },
156
+ }.to_json,
157
+ headers: response_headers
158
+ )
159
+ end
160
+
161
+ it 'fetches the already-created resource' do
162
+ post_create_response
163
+ expect(post_stub).to have_been_requested
164
+ expect(get_stub).to have_been_requested
165
+ end
166
+ end
167
+ end
168
+
169
+ describe '#create' do
170
+ subject(:post_create_response) { client.instalment_schedules.create_with_schedule(params: new_resource) }
14
171
  context 'with a valid request' do
15
172
  let(:new_resource) do
16
173
  {
@@ -372,6 +529,41 @@ describe GoCardlessPro::Resources::InstalmentSchedule do
372
529
  end
373
530
  end
374
531
 
532
+ describe '#update' do
533
+ subject(:put_update_response) { client.instalment_schedules.update(id, params: update_params) }
534
+ let(:id) { 'ABC123' }
535
+
536
+ context 'with a valid request' do
537
+ let(:update_params) { { 'hello' => 'world' } }
538
+
539
+ let!(:stub) do
540
+ stub_url = '/instalment_schedules/:identity'.gsub(':identity', id)
541
+ stub_request(:put, /.*api.gocardless.com#{stub_url}/).to_return(
542
+ body: {
543
+ 'instalment_schedules' => {
544
+
545
+ 'created_at' => 'created_at-input',
546
+ 'currency' => 'currency-input',
547
+ 'id' => 'id-input',
548
+ 'links' => 'links-input',
549
+ 'metadata' => 'metadata-input',
550
+ 'name' => 'name-input',
551
+ 'payment_errors' => 'payment_errors-input',
552
+ 'status' => 'status-input',
553
+ 'total_amount' => 'total_amount-input',
554
+ },
555
+ }.to_json,
556
+ headers: response_headers
557
+ )
558
+ end
559
+
560
+ it 'updates and returns the resource' do
561
+ expect(put_update_response).to be_a(GoCardlessPro::Resources::InstalmentSchedule)
562
+ expect(stub).to have_been_requested
563
+ end
564
+ end
565
+ end
566
+
375
567
  describe '#cancel' do
376
568
  subject(:post_response) { client.instalment_schedules.cancel(resource_id) }
377
569