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.
- checksums.yaml +4 -4
- data/lib/gocardless_pro.rb +3 -0
- data/lib/gocardless_pro/client.rb +6 -1
- data/lib/gocardless_pro/resources/creditor_bank_account.rb +1 -2
- data/lib/gocardless_pro/resources/currency_exchange_rate.rb +44 -0
- data/lib/gocardless_pro/resources/customer_notification.rb +3 -5
- data/lib/gocardless_pro/resources/event.rb +2 -1
- data/lib/gocardless_pro/resources/mandate_import.rb +5 -8
- data/lib/gocardless_pro/resources/mandate_import_entry.rb +3 -5
- data/lib/gocardless_pro/resources/payout.rb +2 -0
- data/lib/gocardless_pro/resources/redirect_flow.rb +2 -0
- data/lib/gocardless_pro/resources/subscription.rb +38 -29
- data/lib/gocardless_pro/services/currency_exchange_rates_service.rb +67 -0
- data/lib/gocardless_pro/services/customers_service.rb +1 -2
- data/lib/gocardless_pro/services/instalment_schedules_service.rb +81 -5
- data/lib/gocardless_pro/services/mandates_service.rb +1 -1
- data/lib/gocardless_pro/services/payouts_service.rb +21 -0
- data/lib/gocardless_pro/services/subscriptions_service.rb +129 -0
- data/lib/gocardless_pro/version.rb +1 -1
- data/spec/resources/currency_exchange_rate_spec.rb +103 -0
- data/spec/resources/instalment_schedule_spec.rb +193 -1
- data/spec/resources/payout_spec.rb +45 -0
- data/spec/resources/redirect_flow_spec.rb +9 -0
- data/spec/resources/subscription_spec.rb +210 -0
- data/spec/services/currency_exchange_rates_service_spec.rb +223 -0
- data/spec/services/instalment_schedules_service_spec.rb +270 -1
- data/spec/services/payouts_service_spec.rb +74 -0
- data/spec/services/redirect_flows_service_spec.rb +9 -0
- data/spec/services/subscriptions_service_spec.rb +240 -0
- 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
|
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.
|
@@ -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.
|
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
|
|